mirror of
https://fastgit.cc/github.com/sourcegit-scm/sourcegit
synced 2026-04-21 13:20:30 +08:00
1374 lines
59 KiB
C#
1374 lines
59 KiB
C#
using System;
|
|
using System.IO;
|
|
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace SourceGit.Views
|
|
{
|
|
public partial class WorkingCopy : UserControl
|
|
{
|
|
public WorkingCopy()
|
|
{
|
|
InitializeComponent();
|
|
}
|
|
|
|
private void OnMainLayoutSizeChanged(object sender, SizeChangedEventArgs e)
|
|
{
|
|
if (sender is not Grid grid)
|
|
return;
|
|
|
|
var layout = ViewModels.Preferences.Instance.Layout;
|
|
var width = grid.Bounds.Width;
|
|
var maxLeft = width - 304;
|
|
|
|
if (layout.WorkingCopyLeftWidth.Value - maxLeft > 1.0)
|
|
layout.WorkingCopyLeftWidth = new GridLength(maxLeft, GridUnitType.Pixel);
|
|
}
|
|
|
|
private async void OnOpenAssumeUnchanged(object sender, RoutedEventArgs e)
|
|
{
|
|
var repoView = this.FindAncestorOfType<Repository>();
|
|
if (repoView is { DataContext: ViewModels.Repository repo })
|
|
await App.ShowDialog(new ViewModels.AssumeUnchangedManager(repo));
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OnUnstagedContextRequested(object sender, ContextRequestedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm && sender is Control control)
|
|
{
|
|
var container = control.FindDescendantOfType<ChangeCollectionContainer>();
|
|
var selectedSingleFolder = string.Empty;
|
|
if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node })
|
|
selectedSingleFolder = node.FullPath;
|
|
|
|
var menu = CreateContextMenuForUnstagedChanges(vm, selectedSingleFolder);
|
|
menu?.Open(control);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void OnStagedContextRequested(object sender, ContextRequestedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm && sender is Control control)
|
|
{
|
|
var container = control.FindDescendantOfType<ChangeCollectionContainer>();
|
|
var selectedSingleFolder = string.Empty;
|
|
if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node })
|
|
selectedSingleFolder = node.FullPath;
|
|
|
|
var menu = CreateContextMenuForStagedChanges(vm, selectedSingleFolder);
|
|
menu?.Open(control);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private async void OnUnstagedChangeDoubleTapped(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
{
|
|
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
|
|
await vm.StageChangesAsync(vm.SelectedUnstaged, next);
|
|
UnstagedChangesView.TakeFocus();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private async void OnStagedChangeDoubleTapped(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
{
|
|
var next = StagedChangesView.GetNextChangeWithoutSelection();
|
|
await vm.UnstageChangesAsync(vm.SelectedStaged, next);
|
|
StagedChangesView.TakeFocus();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private async void OnUnstagedKeyDown(object _, KeyEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
{
|
|
var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
|
|
|
|
if (e.Key is Key.Space or Key.Enter)
|
|
{
|
|
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
|
|
await vm.StageChangesAsync(vm.SelectedUnstaged, next);
|
|
UnstagedChangesView.TakeFocus();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key is Key.Delete or Key.Back && vm.SelectedUnstaged is { Count: > 0 })
|
|
{
|
|
vm.Discard(vm.SelectedUnstaged);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key is Key.O && e.KeyModifiers == cmdKey && vm.SelectedUnstaged is { Count: 1 })
|
|
{
|
|
var change = vm.SelectedUnstaged[0];
|
|
var fullpath = Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path);
|
|
if (File.Exists(fullpath))
|
|
Native.OS.OpenWithDefaultEditor(fullpath);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key is Key.C && e.KeyModifiers.HasFlag(cmdKey) && vm.SelectedUnstaged is { Count: 1 })
|
|
{
|
|
var change = vm.SelectedUnstaged[0];
|
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
|
await App.CopyTextAsync(Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path));
|
|
else
|
|
await App.CopyTextAsync(change.Path);
|
|
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key is Key.F && e.KeyModifiers == cmdKey)
|
|
{
|
|
LocalChangesSearchBox.Focus();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void OnStagedKeyDown(object _, KeyEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
{
|
|
var cmdKey = OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
|
|
|
|
if (e.Key is Key.Space or Key.Enter)
|
|
{
|
|
var next = StagedChangesView.GetNextChangeWithoutSelection();
|
|
await vm.UnstageChangesAsync(vm.SelectedStaged, next);
|
|
StagedChangesView.TakeFocus();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key is Key.O && e.KeyModifiers == cmdKey && vm.SelectedStaged is { Count: 1 })
|
|
{
|
|
var change = vm.SelectedStaged[0];
|
|
var fullpath = Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path);
|
|
if (File.Exists(fullpath))
|
|
Native.OS.OpenWithDefaultEditor(fullpath);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key is Key.C && e.KeyModifiers.HasFlag(cmdKey) && vm.SelectedStaged is { Count: 1 })
|
|
{
|
|
var change = vm.SelectedStaged[0];
|
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
|
await App.CopyTextAsync(Native.OS.GetAbsPath(vm.Repository.FullPath, change.Path));
|
|
else
|
|
await App.CopyTextAsync(change.Path);
|
|
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key is Key.F && e.KeyModifiers == cmdKey)
|
|
{
|
|
LocalChangesSearchBox.Focus();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void OnStageSelectedButtonClicked(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
{
|
|
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
|
|
await vm.StageChangesAsync(vm.SelectedUnstaged, next);
|
|
UnstagedChangesView.TakeFocus();
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnStageAllButtonClicked(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
await vm.StageChangesAsync(vm.VisibleUnstaged, null);
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnUnstageSelectedButtonClicked(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
{
|
|
var next = StagedChangesView.GetNextChangeWithoutSelection();
|
|
await vm.UnstageChangesAsync(vm.SelectedStaged, next);
|
|
StagedChangesView.TakeFocus();
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnUnstageAllButtonClicked(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
await vm.UnstageChangesAsync(vm.VisibleStaged, null);
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnOpenExternalMergeToolAllConflicts(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
await vm.UseExternalMergeToolAsync(null);
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnContinue(object _, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
await vm.ContinueMergeAsync();
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnCommit(object _, RoutedEventArgs e)
|
|
{
|
|
if (App.GetLauncher() is { CommandPalette: { } } launcher)
|
|
return;
|
|
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
await vm.CommitAsync(false, false);
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnCommitWithAutoStage(object _, RoutedEventArgs e)
|
|
{
|
|
if (App.GetLauncher() is { CommandPalette: { } } launcher)
|
|
return;
|
|
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
await vm.CommitAsync(true, false);
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnCommitWithPush(object _, RoutedEventArgs e)
|
|
{
|
|
if (App.GetLauncher() is { CommandPalette: { } } launcher)
|
|
return;
|
|
|
|
if (DataContext is ViewModels.WorkingCopy vm)
|
|
await vm.CommitAsync(false, true);
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private ContextMenu CreateContextMenuForUnstagedChanges(ViewModels.WorkingCopy vm, string selectedSingleFolder)
|
|
{
|
|
var repo = vm.Repository;
|
|
var selectedUnstaged = vm.SelectedUnstaged;
|
|
if (repo == null || selectedUnstaged == null || selectedUnstaged.Count == 0)
|
|
return null;
|
|
|
|
var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder);
|
|
var menu = new ContextMenu();
|
|
if (selectedUnstaged.Count == 1)
|
|
{
|
|
var change = selectedUnstaged[0];
|
|
var path = Native.OS.GetAbsPath(repo.FullPath, change.Path);
|
|
|
|
if (!change.IsConflicted)
|
|
{
|
|
TryAddOpenFileToContextMenu(menu, path);
|
|
|
|
var diffWithMerger = new MenuItem();
|
|
diffWithMerger.Header = App.Text("OpenInExternalMergeTool");
|
|
diffWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
|
|
diffWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
|
|
diffWithMerger.Click += (_, ev) =>
|
|
{
|
|
vm.UseExternalDiffTool(change, true);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(diffWithMerger);
|
|
}
|
|
|
|
var explore = new MenuItem();
|
|
explore.Header = App.Text("RevealFile");
|
|
explore.Icon = App.CreateMenuIcon("Icons.Explore");
|
|
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
|
|
explore.Click += (_, e) =>
|
|
{
|
|
var target = hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path;
|
|
Native.OS.OpenInFileManager(target);
|
|
e.Handled = true;
|
|
};
|
|
menu.Items.Add(explore);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
|
|
if (change.IsConflicted)
|
|
{
|
|
var useTheirs = new MenuItem();
|
|
useTheirs.Icon = App.CreateMenuIcon("Icons.Incoming");
|
|
useTheirs.Click += async (_, e) =>
|
|
{
|
|
await vm.UseTheirsAsync(selectedUnstaged);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var useMine = new MenuItem();
|
|
useMine.Icon = App.CreateMenuIcon("Icons.Local");
|
|
useMine.Click += async (_, e) =>
|
|
{
|
|
await vm.UseMineAsync(selectedUnstaged);
|
|
e.Handled = true;
|
|
};
|
|
|
|
switch (vm.InProgressContext)
|
|
{
|
|
case ViewModels.CherryPickInProgress cherryPick:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", cherryPick.HeadName);
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", repo.CurrentBranch.Name);
|
|
break;
|
|
case ViewModels.RebaseInProgress rebase:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", rebase.HeadName);
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", rebase.BaseName);
|
|
break;
|
|
case ViewModels.RevertInProgress revert:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", $"{revert.Head.SHA.AsSpan(0, 10)} (revert)");
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", repo.CurrentBranch.Name);
|
|
break;
|
|
case ViewModels.MergeInProgress merge:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", merge.SourceName);
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", repo.CurrentBranch.Name);
|
|
break;
|
|
default:
|
|
useTheirs.Header = App.Text("FileCM.UseTheirs");
|
|
useMine.Header = App.Text("FileCM.UseMine");
|
|
break;
|
|
}
|
|
|
|
menu.Items.Add(useTheirs);
|
|
menu.Items.Add(useMine);
|
|
|
|
if (change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified && !Directory.Exists(path))
|
|
{
|
|
var mergeBuiltin = new MenuItem();
|
|
mergeBuiltin.Header = App.Text("ChangeCM.Merge");
|
|
mergeBuiltin.Icon = App.CreateMenuIcon("Icons.Conflict");
|
|
mergeBuiltin.Click += async (_, e) =>
|
|
{
|
|
var head = await new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResultAsync();
|
|
await App.ShowDialog(new ViewModels.MergeConflictEditor(repo, head, change.Path));
|
|
e.Handled = true;
|
|
};
|
|
|
|
var mergeExternal = new MenuItem();
|
|
mergeExternal.Header = App.Text("ChangeCM.MergeExternal");
|
|
mergeExternal.Icon = App.CreateMenuIcon("Icons.OpenWith");
|
|
mergeExternal.Click += async (_, e) =>
|
|
{
|
|
await vm.UseExternalMergeToolAsync(change);
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(mergeBuiltin);
|
|
menu.Items.Add(mergeExternal);
|
|
}
|
|
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
else
|
|
{
|
|
var stage = new MenuItem();
|
|
stage.Header = App.Text("FileCM.Stage");
|
|
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
|
|
stage.Tag = "Enter/Space";
|
|
stage.Click += async (_, e) =>
|
|
{
|
|
await vm.StageChangesAsync(selectedUnstaged, null);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var discard = new MenuItem();
|
|
discard.Header = App.Text("FileCM.Discard");
|
|
discard.Icon = App.CreateMenuIcon("Icons.Undo");
|
|
discard.Tag = "Back/Delete";
|
|
discard.Click += (_, e) =>
|
|
{
|
|
vm.Discard(selectedUnstaged);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var stash = new MenuItem();
|
|
stash.Header = App.Text("FileCM.Stash");
|
|
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
|
|
stash.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.StashChanges(repo, selectedUnstaged));
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
var patch = new MenuItem();
|
|
patch.Header = App.Text("FileCM.SaveAsPatch");
|
|
patch.Icon = App.CreateMenuIcon("Icons.Save");
|
|
patch.Click += async (_, e) =>
|
|
{
|
|
var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
|
|
if (storageProvider == null)
|
|
return;
|
|
|
|
var options = new FilePickerSaveOptions();
|
|
options.Title = App.Text("FileCM.SaveAsPatch");
|
|
options.DefaultExtension = ".patch";
|
|
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
|
|
|
|
try
|
|
{
|
|
var storageFile = await storageProvider.SaveFilePickerAsync(options);
|
|
if (storageFile != null)
|
|
await vm.SaveChangesToPatchAsync(selectedUnstaged, true, storageFile.Path.LocalPath);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
App.RaiseException(repo.FullPath, $"Failed to save as patch: {exception.Message}");
|
|
}
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
var assumeUnchanged = new MenuItem();
|
|
assumeUnchanged.Header = App.Text("FileCM.AssumeUnchanged");
|
|
assumeUnchanged.Icon = App.CreateMenuIcon("Icons.File.Ignore");
|
|
assumeUnchanged.IsVisible = change.WorkTree != Models.ChangeState.Untracked;
|
|
assumeUnchanged.Click += async (_, e) =>
|
|
{
|
|
var log = repo.CreateLog("Assume File Unchanged");
|
|
await new Commands.AssumeUnchanged(repo.FullPath, change.Path, true).Use(log).ExecAsync();
|
|
log.Complete();
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(stage);
|
|
menu.Items.Add(discard);
|
|
menu.Items.Add(stash);
|
|
menu.Items.Add(patch);
|
|
menu.Items.Add(assumeUnchanged);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
|
|
var extension = Path.GetExtension(change.Path);
|
|
var hasExtra = false;
|
|
if (change.WorkTree == Models.ChangeState.Untracked)
|
|
{
|
|
var addToIgnore = new MenuItem();
|
|
addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore");
|
|
addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore");
|
|
|
|
if (hasSelectedFolder)
|
|
{
|
|
var ignoreFolder = new MenuItem();
|
|
ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder");
|
|
ignoreFolder.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selectedSingleFolder}/"));
|
|
e.Handled = true;
|
|
};
|
|
addToIgnore.Items.Add(ignoreFolder);
|
|
}
|
|
else
|
|
{
|
|
var isRooted = change.Path!.IndexOf('/') <= 0;
|
|
var singleFile = new MenuItem();
|
|
singleFile.Header = App.Text("WorkingCopy.AddToGitIgnore.SingleFile");
|
|
singleFile.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.AddToIgnore(repo, change.Path));
|
|
e.Handled = true;
|
|
};
|
|
addToIgnore.Items.Add(singleFile);
|
|
|
|
if (!string.IsNullOrEmpty(extension))
|
|
{
|
|
var byExtension = new MenuItem();
|
|
byExtension.Header = App.Text("WorkingCopy.AddToGitIgnore.Extension", extension);
|
|
byExtension.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"*{extension}"));
|
|
e.Handled = true;
|
|
};
|
|
addToIgnore.Items.Add(byExtension);
|
|
|
|
var byExtensionInSameFolder = new MenuItem();
|
|
byExtensionInSameFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.ExtensionInSameFolder", extension);
|
|
byExtensionInSameFolder.IsVisible = !isRooted;
|
|
byExtensionInSameFolder.Click += (_, e) =>
|
|
{
|
|
var dir = Path.GetDirectoryName(change.Path)!.Replace('\\', '/').TrimEnd('/');
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{dir}/*{extension}"));
|
|
e.Handled = true;
|
|
};
|
|
addToIgnore.Items.Add(byExtensionInSameFolder);
|
|
}
|
|
}
|
|
|
|
menu.Items.Add(addToIgnore);
|
|
hasExtra = true;
|
|
}
|
|
else if (hasSelectedFolder)
|
|
{
|
|
var addToIgnore = new MenuItem();
|
|
addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore");
|
|
addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore");
|
|
|
|
var ignoreFolder = new MenuItem();
|
|
ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder");
|
|
ignoreFolder.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selectedSingleFolder}/"));
|
|
e.Handled = true;
|
|
};
|
|
addToIgnore.Items.Add(ignoreFolder);
|
|
|
|
menu.Items.Add(addToIgnore);
|
|
hasExtra = true;
|
|
}
|
|
|
|
if (repo.IsLFSEnabled())
|
|
{
|
|
var lfs = new MenuItem();
|
|
lfs.Header = App.Text("GitLFS");
|
|
lfs.Icon = App.CreateMenuIcon("Icons.LFS");
|
|
|
|
var isLFSFiltered = new Commands.IsLFSFiltered(repo.FullPath, change.Path).GetResult();
|
|
if (!isLFSFiltered)
|
|
{
|
|
var filename = Path.GetFileName(change.Path);
|
|
var lfsTrackThisFile = new MenuItem();
|
|
lfsTrackThisFile.Header = App.Text("GitLFS.Track", filename);
|
|
lfsTrackThisFile.Click += async (_, e) =>
|
|
{
|
|
await repo.TrackLFSFileAsync(filename, true);
|
|
e.Handled = true;
|
|
};
|
|
lfs.Items.Add(lfsTrackThisFile);
|
|
|
|
if (!string.IsNullOrEmpty(extension))
|
|
{
|
|
var lfsTrackByExtension = new MenuItem();
|
|
lfsTrackByExtension.Header = App.Text("GitLFS.TrackByExtension", extension);
|
|
lfsTrackByExtension.Click += async (_, e) =>
|
|
{
|
|
await repo.TrackLFSFileAsync($"*{extension}", false);
|
|
e.Handled = true;
|
|
};
|
|
lfs.Items.Add(lfsTrackByExtension);
|
|
}
|
|
|
|
lfs.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
var lfsLock = new MenuItem();
|
|
lfsLock.Header = App.Text("GitLFS.Locks.Lock");
|
|
lfsLock.Icon = App.CreateMenuIcon("Icons.Lock");
|
|
lfsLock.IsEnabled = repo.Remotes.Count > 0;
|
|
if (repo.Remotes.Count == 1)
|
|
{
|
|
lfsLock.Click += async (_, e) =>
|
|
{
|
|
await repo.LockLFSFileAsync(repo.Remotes[0].Name, change.Path);
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
foreach (var remote in repo.Remotes)
|
|
{
|
|
var remoteName = remote.Name;
|
|
var lockRemote = new MenuItem();
|
|
lockRemote.Header = remoteName;
|
|
lockRemote.Click += async (_, e) =>
|
|
{
|
|
await repo.LockLFSFileAsync(remoteName, change.Path);
|
|
e.Handled = true;
|
|
};
|
|
lfsLock.Items.Add(lockRemote);
|
|
}
|
|
}
|
|
lfs.Items.Add(lfsLock);
|
|
|
|
var lfsUnlock = new MenuItem();
|
|
lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock");
|
|
lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock");
|
|
lfsUnlock.IsEnabled = repo.Remotes.Count > 0;
|
|
if (repo.Remotes.Count == 1)
|
|
{
|
|
lfsUnlock.Click += async (_, e) =>
|
|
{
|
|
await repo.UnlockLFSFileAsync(repo.Remotes[0].Name, change.Path, false, true);
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
foreach (var remote in repo.Remotes)
|
|
{
|
|
var remoteName = remote.Name;
|
|
var unlockRemote = new MenuItem();
|
|
unlockRemote.Header = remoteName;
|
|
unlockRemote.Click += async (_, e) =>
|
|
{
|
|
await repo.UnlockLFSFileAsync(remoteName, change.Path, false, true);
|
|
e.Handled = true;
|
|
};
|
|
lfsUnlock.Items.Add(unlockRemote);
|
|
}
|
|
}
|
|
lfs.Items.Add(lfsUnlock);
|
|
|
|
menu.Items.Add(lfs);
|
|
hasExtra = true;
|
|
}
|
|
|
|
if (hasExtra)
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
if (hasSelectedFolder)
|
|
{
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("DirHistories");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, e) =>
|
|
{
|
|
App.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder));
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
else if (change.WorkTree is not (Models.ChangeState.Untracked or Models.ChangeState.Added))
|
|
{
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("FileHistory");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, e) =>
|
|
{
|
|
App.ShowWindow(new ViewModels.FileHistories(repo.FullPath, change.Path));
|
|
e.Handled = true;
|
|
};
|
|
|
|
var blame = new MenuItem();
|
|
blame.Header = App.Text("Blame") + " (HEAD-only)";
|
|
blame.Icon = App.CreateMenuIcon("Icons.Blame");
|
|
blame.Click += async (_, ev) =>
|
|
{
|
|
var commit = await new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResultAsync();
|
|
App.ShowWindow(new ViewModels.Blame(repo.FullPath, change.Path, commit));
|
|
ev.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(blame);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
TryToAddCustomActionsToContextMenu(repo, menu, change.Path);
|
|
|
|
var copy = new MenuItem();
|
|
copy.Header = App.Text("CopyPath");
|
|
copy.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copy.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
|
|
copy.Click += async (_, e) =>
|
|
{
|
|
await App.CopyTextAsync(hasSelectedFolder ? selectedSingleFolder : change.Path);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var copyFullPath = new MenuItem();
|
|
copyFullPath.Header = App.Text("CopyFullPath");
|
|
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
|
|
copyFullPath.Click += async (_, e) =>
|
|
{
|
|
await App.CopyTextAsync(hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path);
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(copy);
|
|
menu.Items.Add(copyFullPath);
|
|
}
|
|
else
|
|
{
|
|
var hasConflicts = false;
|
|
var hasNonConflicts = false;
|
|
foreach (var change in selectedUnstaged)
|
|
{
|
|
if (change.IsConflicted)
|
|
hasConflicts = true;
|
|
else
|
|
hasNonConflicts = true;
|
|
}
|
|
|
|
if (hasConflicts)
|
|
{
|
|
if (hasNonConflicts)
|
|
{
|
|
App.RaiseException(repo.FullPath, "Selection contains both conflict and non-conflict changes!");
|
|
return null;
|
|
}
|
|
|
|
var useTheirs = new MenuItem();
|
|
useTheirs.Icon = App.CreateMenuIcon("Icons.Incoming");
|
|
useTheirs.Click += async (_, e) =>
|
|
{
|
|
await vm.UseTheirsAsync(selectedUnstaged);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var useMine = new MenuItem();
|
|
useMine.Icon = App.CreateMenuIcon("Icons.Local");
|
|
useMine.Click += async (_, e) =>
|
|
{
|
|
await vm.UseMineAsync(selectedUnstaged);
|
|
e.Handled = true;
|
|
};
|
|
|
|
switch (vm.InProgressContext)
|
|
{
|
|
case ViewModels.CherryPickInProgress cherryPick:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", cherryPick.HeadName);
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", repo.CurrentBranch.Name);
|
|
break;
|
|
case ViewModels.RebaseInProgress rebase:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", rebase.HeadName);
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", rebase.BaseName);
|
|
break;
|
|
case ViewModels.RevertInProgress revert:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", $"{revert.Head.SHA.AsSpan(0, 10)} (revert)");
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", repo.CurrentBranch.Name);
|
|
break;
|
|
case ViewModels.MergeInProgress merge:
|
|
useTheirs.Header = App.Text("FileCM.ResolveUsing", merge.SourceName);
|
|
useMine.Header = App.Text("FileCM.ResolveUsing", repo.CurrentBranch.Name);
|
|
break;
|
|
default:
|
|
useTheirs.Header = App.Text("FileCM.UseTheirs");
|
|
useMine.Header = App.Text("FileCM.UseMine");
|
|
break;
|
|
}
|
|
|
|
menu.Items.Add(useTheirs);
|
|
menu.Items.Add(useMine);
|
|
return menu;
|
|
}
|
|
|
|
if (hasSelectedFolder)
|
|
{
|
|
var dir = Path.Combine(repo.FullPath, selectedSingleFolder);
|
|
var explore = new MenuItem();
|
|
explore.Header = App.Text("RevealFile");
|
|
explore.Icon = App.CreateMenuIcon("Icons.Explore");
|
|
explore.IsEnabled = Directory.Exists(dir);
|
|
explore.Click += (_, e) =>
|
|
{
|
|
Native.OS.OpenInFileManager(dir);
|
|
e.Handled = true;
|
|
};
|
|
menu.Items.Add(explore);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
var stage = new MenuItem();
|
|
stage.Header = App.Text("FileCM.StageMulti", selectedUnstaged.Count);
|
|
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
|
|
stage.Tag = "Enter/Space";
|
|
stage.Click += async (_, e) =>
|
|
{
|
|
await vm.StageChangesAsync(selectedUnstaged, null);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var discard = new MenuItem();
|
|
discard.Header = App.Text("FileCM.DiscardMulti", selectedUnstaged.Count);
|
|
discard.Icon = App.CreateMenuIcon("Icons.Undo");
|
|
discard.Tag = "Back/Delete";
|
|
discard.Click += (_, e) =>
|
|
{
|
|
vm.Discard(selectedUnstaged);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var stash = new MenuItem();
|
|
stash.Header = App.Text("FileCM.StashMulti", selectedUnstaged.Count);
|
|
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
|
|
stash.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.StashChanges(repo, selectedUnstaged));
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
var patch = new MenuItem();
|
|
patch.Header = App.Text("FileCM.SaveAsPatch");
|
|
patch.Icon = App.CreateMenuIcon("Icons.Save");
|
|
patch.Click += async (_, e) =>
|
|
{
|
|
var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
|
|
if (storageProvider == null)
|
|
return;
|
|
|
|
var options = new FilePickerSaveOptions();
|
|
options.Title = App.Text("FileCM.SaveAsPatch");
|
|
options.DefaultExtension = ".patch";
|
|
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
|
|
|
|
try
|
|
{
|
|
var storageFile = await storageProvider.SaveFilePickerAsync(options);
|
|
if (storageFile != null)
|
|
await vm.SaveChangesToPatchAsync(selectedUnstaged, true, storageFile.Path.LocalPath);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
App.RaiseException(repo.FullPath, $"Failed to save as patch: {exception.Message}");
|
|
}
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(stage);
|
|
menu.Items.Add(discard);
|
|
menu.Items.Add(stash);
|
|
menu.Items.Add(patch);
|
|
|
|
if (hasSelectedFolder)
|
|
{
|
|
var ignoreFolder = new MenuItem();
|
|
ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder");
|
|
ignoreFolder.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.AddToIgnore(repo, $"{selectedSingleFolder}/"));
|
|
e.Handled = true;
|
|
};
|
|
|
|
var addToIgnore = new MenuItem();
|
|
addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore");
|
|
addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore");
|
|
addToIgnore.Items.Add(ignoreFolder);
|
|
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("DirHistories");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, e) =>
|
|
{
|
|
App.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder));
|
|
e.Handled = true;
|
|
};
|
|
|
|
var copy = new MenuItem();
|
|
copy.Header = App.Text("CopyPath");
|
|
copy.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copy.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
|
|
copy.Click += async (_, e) =>
|
|
{
|
|
await App.CopyTextAsync(selectedSingleFolder);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var copyFullPath = new MenuItem();
|
|
copyFullPath.Header = App.Text("CopyPath");
|
|
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
|
|
copyFullPath.Click += async (_, e) =>
|
|
{
|
|
await App.CopyTextAsync(Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder));
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
menu.Items.Add(addToIgnore);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
menu.Items.Add(copy);
|
|
menu.Items.Add(copyFullPath);
|
|
}
|
|
}
|
|
|
|
return menu;
|
|
}
|
|
|
|
public ContextMenu CreateContextMenuForStagedChanges(ViewModels.WorkingCopy vm, string selectedSingleFolder)
|
|
{
|
|
var repo = vm.Repository;
|
|
var selectedStaged = vm.SelectedStaged;
|
|
if (repo == null || selectedStaged == null || selectedStaged.Count == 0)
|
|
return null;
|
|
|
|
var menu = new ContextMenu();
|
|
|
|
MenuItem ai = null;
|
|
var services = repo.GetPreferredOpenAIServices();
|
|
if (services.Count > 0)
|
|
{
|
|
ai = new MenuItem();
|
|
ai.Icon = App.CreateMenuIcon("Icons.AIAssist");
|
|
ai.Header = App.Text("ChangeCM.GenerateCommitMessage");
|
|
|
|
if (services.Count == 1)
|
|
{
|
|
ai.Click += async (_, e) =>
|
|
{
|
|
await App.ShowDialog(new ViewModels.AIAssistant(repo, services[0], selectedStaged));
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
foreach (var service in services)
|
|
{
|
|
var dup = service;
|
|
|
|
var item = new MenuItem();
|
|
item.Header = service.Name;
|
|
item.Click += async (_, e) =>
|
|
{
|
|
await App.ShowDialog(new ViewModels.AIAssistant(repo, dup, selectedStaged));
|
|
e.Handled = true;
|
|
};
|
|
|
|
ai.Items.Add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder);
|
|
if (selectedStaged.Count == 1)
|
|
{
|
|
var change = selectedStaged[0];
|
|
var path = Native.OS.GetAbsPath(repo.FullPath, change.Path);
|
|
|
|
var openWithMerger = new MenuItem();
|
|
openWithMerger.Header = App.Text("OpenInExternalMergeTool");
|
|
openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
|
|
openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D";
|
|
openWithMerger.Click += (_, ev) =>
|
|
{
|
|
vm.UseExternalDiffTool(change, false);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var explore = new MenuItem();
|
|
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
|
|
explore.Header = App.Text("RevealFile");
|
|
explore.Icon = App.CreateMenuIcon("Icons.Explore");
|
|
explore.Click += (_, e) =>
|
|
{
|
|
var target = hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path;
|
|
Native.OS.OpenInFileManager(target);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var unstage = new MenuItem();
|
|
unstage.Header = App.Text("FileCM.Unstage");
|
|
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
|
|
unstage.Tag = "Enter/Space";
|
|
unstage.Click += async (_, e) =>
|
|
{
|
|
await vm.UnstageChangesAsync(selectedStaged, null);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var stash = new MenuItem();
|
|
stash.Header = App.Text("FileCM.Stash");
|
|
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
|
|
stash.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.StashChanges(repo, selectedStaged));
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
var patch = new MenuItem();
|
|
patch.Header = App.Text("FileCM.SaveAsPatch");
|
|
patch.Icon = App.CreateMenuIcon("Icons.Save");
|
|
patch.Click += async (_, e) =>
|
|
{
|
|
var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
|
|
if (storageProvider == null)
|
|
return;
|
|
|
|
var options = new FilePickerSaveOptions();
|
|
options.Title = App.Text("FileCM.SaveAsPatch");
|
|
options.DefaultExtension = ".patch";
|
|
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
|
|
|
|
try
|
|
{
|
|
var storageFile = await storageProvider.SaveFilePickerAsync(options);
|
|
if (storageFile != null)
|
|
await vm.SaveChangesToPatchAsync(selectedStaged, false, storageFile.Path.LocalPath);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
App.RaiseException(repo.FullPath, $"Failed to save as patch: {exception.Message}");
|
|
}
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
TryAddOpenFileToContextMenu(menu, path);
|
|
menu.Items.Add(openWithMerger);
|
|
menu.Items.Add(explore);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
menu.Items.Add(unstage);
|
|
menu.Items.Add(stash);
|
|
menu.Items.Add(patch);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
|
|
if (repo.IsLFSEnabled())
|
|
{
|
|
var lfs = new MenuItem();
|
|
lfs.Header = App.Text("GitLFS");
|
|
lfs.Icon = App.CreateMenuIcon("Icons.LFS");
|
|
|
|
var lfsLock = new MenuItem();
|
|
lfsLock.Header = App.Text("GitLFS.Locks.Lock");
|
|
lfsLock.Icon = App.CreateMenuIcon("Icons.Lock");
|
|
lfsLock.IsEnabled = repo.Remotes.Count > 0;
|
|
if (repo.Remotes.Count == 1)
|
|
{
|
|
lfsLock.Click += async (_, e) =>
|
|
{
|
|
await repo.LockLFSFileAsync(repo.Remotes[0].Name, change.Path);
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
foreach (var remote in repo.Remotes)
|
|
{
|
|
var remoteName = remote.Name;
|
|
var lockRemote = new MenuItem();
|
|
lockRemote.Header = remoteName;
|
|
lockRemote.Click += async (_, e) =>
|
|
{
|
|
await repo.LockLFSFileAsync(remoteName, change.Path);
|
|
e.Handled = true;
|
|
};
|
|
lfsLock.Items.Add(lockRemote);
|
|
}
|
|
}
|
|
lfs.Items.Add(lfsLock);
|
|
|
|
var lfsUnlock = new MenuItem();
|
|
lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock");
|
|
lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock");
|
|
lfsUnlock.IsEnabled = repo.Remotes.Count > 0;
|
|
if (repo.Remotes.Count == 1)
|
|
{
|
|
lfsUnlock.Click += async (_, e) =>
|
|
{
|
|
await repo.UnlockLFSFileAsync(repo.Remotes[0].Name, change.Path, false, true);
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
foreach (var remote in repo.Remotes)
|
|
{
|
|
var remoteName = remote.Name;
|
|
var unlockRemote = new MenuItem();
|
|
unlockRemote.Header = remoteName;
|
|
unlockRemote.Click += async (_, e) =>
|
|
{
|
|
await repo.UnlockLFSFileAsync(remoteName, change.Path, false, true);
|
|
e.Handled = true;
|
|
};
|
|
lfsUnlock.Items.Add(unlockRemote);
|
|
}
|
|
}
|
|
lfs.Items.Add(lfsUnlock);
|
|
|
|
menu.Items.Add(lfs);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
if (ai != null)
|
|
{
|
|
menu.Items.Add(ai);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
if (hasSelectedFolder)
|
|
{
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("DirHistories");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, e) =>
|
|
{
|
|
App.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder));
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
else if (change.Index is not (Models.ChangeState.Added or Models.ChangeState.Renamed))
|
|
{
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("FileHistory");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, e) =>
|
|
{
|
|
App.ShowWindow(new ViewModels.FileHistories(repo.FullPath, change.Path));
|
|
e.Handled = true;
|
|
};
|
|
|
|
var blame = new MenuItem();
|
|
blame.Header = App.Text("Blame") + " (HEAD-only)";
|
|
blame.Icon = App.CreateMenuIcon("Icons.Blame");
|
|
blame.Click += async (_, e) =>
|
|
{
|
|
var commit = await new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResultAsync();
|
|
App.ShowWindow(new ViewModels.Blame(repo.FullPath, change.Path, commit));
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(blame);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
TryToAddCustomActionsToContextMenu(repo, menu, change.Path);
|
|
|
|
var copyPath = new MenuItem();
|
|
copyPath.Header = App.Text("CopyPath");
|
|
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
|
|
copyPath.Click += async (_, e) =>
|
|
{
|
|
await App.CopyTextAsync(hasSelectedFolder ? selectedSingleFolder : change.Path);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var copyFullPath = new MenuItem();
|
|
copyFullPath.Header = App.Text("CopyFullPath");
|
|
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
|
|
copyFullPath.Click += async (_, e) =>
|
|
{
|
|
var target = hasSelectedFolder ? Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder) : path;
|
|
await App.CopyTextAsync(target);
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(copyPath);
|
|
menu.Items.Add(copyFullPath);
|
|
}
|
|
else
|
|
{
|
|
if (hasSelectedFolder)
|
|
{
|
|
var dir = Path.Combine(repo.FullPath, selectedSingleFolder);
|
|
var explore = new MenuItem();
|
|
explore.IsEnabled = Directory.Exists(dir);
|
|
explore.Header = App.Text("RevealFile");
|
|
explore.Icon = App.CreateMenuIcon("Icons.Explore");
|
|
explore.Click += (_, e) =>
|
|
{
|
|
Native.OS.OpenInFileManager(dir);
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(explore);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
var unstage = new MenuItem();
|
|
unstage.Header = App.Text("FileCM.UnstageMulti", selectedStaged.Count);
|
|
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
|
|
unstage.Tag = "Enter/Space";
|
|
unstage.Click += async (_, e) =>
|
|
{
|
|
await vm.UnstageChangesAsync(selectedStaged, null);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var stash = new MenuItem();
|
|
stash.Header = App.Text("FileCM.StashMulti", selectedStaged.Count);
|
|
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
|
|
stash.Click += (_, e) =>
|
|
{
|
|
if (repo.CanCreatePopup())
|
|
repo.ShowPopup(new ViewModels.StashChanges(repo, selectedStaged));
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
var patch = new MenuItem();
|
|
patch.Header = App.Text("FileCM.SaveAsPatch");
|
|
patch.Icon = App.CreateMenuIcon("Icons.Save");
|
|
patch.Click += async (_, e) =>
|
|
{
|
|
var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
|
|
if (storageProvider == null)
|
|
return;
|
|
|
|
var options = new FilePickerSaveOptions();
|
|
options.Title = App.Text("FileCM.SaveAsPatch");
|
|
options.DefaultExtension = ".patch";
|
|
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
|
|
|
|
try
|
|
{
|
|
var storageFile = await storageProvider.SaveFilePickerAsync(options);
|
|
if (storageFile != null)
|
|
await vm.SaveChangesToPatchAsync(selectedStaged, false, storageFile.Path.LocalPath);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
App.RaiseException(repo.FullPath, $"Failed to save as patch: {exception.Message}");
|
|
}
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(unstage);
|
|
menu.Items.Add(stash);
|
|
menu.Items.Add(patch);
|
|
|
|
if (ai != null)
|
|
{
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
menu.Items.Add(ai);
|
|
}
|
|
|
|
if (hasSelectedFolder)
|
|
{
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("DirHistories");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, e) =>
|
|
{
|
|
App.ShowWindow(new ViewModels.DirHistories(repo, selectedSingleFolder));
|
|
e.Handled = true;
|
|
};
|
|
|
|
var copyPath = new MenuItem();
|
|
copyPath.Header = App.Text("CopyPath");
|
|
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C";
|
|
copyPath.Click += async (_, e) =>
|
|
{
|
|
await App.CopyTextAsync(selectedSingleFolder);
|
|
e.Handled = true;
|
|
};
|
|
|
|
var copyFullPath = new MenuItem();
|
|
copyFullPath.Header = App.Text("CopyFullPath");
|
|
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C";
|
|
copyFullPath.Click += async (_, e) =>
|
|
{
|
|
await App.CopyTextAsync(Native.OS.GetAbsPath(repo.FullPath, selectedSingleFolder));
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
menu.Items.Add(copyPath);
|
|
menu.Items.Add(copyFullPath);
|
|
}
|
|
}
|
|
|
|
return menu;
|
|
}
|
|
|
|
private void TryAddOpenFileToContextMenu(ContextMenu menu, string fullpath)
|
|
{
|
|
var openWith = new MenuItem();
|
|
openWith.Header = App.Text("Open");
|
|
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
|
|
openWith.IsEnabled = File.Exists(fullpath);
|
|
if (openWith.IsEnabled)
|
|
{
|
|
var defaultEditor = new MenuItem();
|
|
defaultEditor.Header = App.Text("Open.SystemDefaultEditor");
|
|
defaultEditor.Tag = OperatingSystem.IsMacOS() ? "⌘+O" : "Ctrl+O";
|
|
defaultEditor.Click += (_, ev) =>
|
|
{
|
|
Native.OS.OpenWithDefaultEditor(fullpath);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
openWith.Items.Add(defaultEditor);
|
|
|
|
var tools = Native.OS.ExternalTools;
|
|
if (tools.Count > 0)
|
|
{
|
|
openWith.Items.Add(new MenuItem() { Header = "-" });
|
|
|
|
for (var i = 0; i < tools.Count; i++)
|
|
{
|
|
var tool = tools[i];
|
|
var item = new MenuItem();
|
|
item.Header = tool.Name;
|
|
item.Icon = new Image { Width = 16, Height = 16, Source = tool.IconImage };
|
|
item.Click += (_, e) =>
|
|
{
|
|
tool.Launch(fullpath.Quoted());
|
|
e.Handled = true;
|
|
};
|
|
|
|
openWith.Items.Add(item);
|
|
}
|
|
}
|
|
}
|
|
menu.Items.Add(openWith);
|
|
}
|
|
|
|
private void TryToAddCustomActionsToContextMenu(ViewModels.Repository repo, ContextMenu menu, string path)
|
|
{
|
|
var actions = repo.GetCustomActions(Models.CustomActionScope.File);
|
|
if (actions.Count == 0)
|
|
return;
|
|
|
|
var target = new Models.CustomActionTargetFile(path, null);
|
|
var custom = new MenuItem();
|
|
custom.Header = App.Text("FileCM.CustomAction");
|
|
custom.Icon = App.CreateMenuIcon("Icons.Action");
|
|
|
|
foreach (var action in actions)
|
|
{
|
|
var (dup, label) = action;
|
|
var item = new MenuItem();
|
|
item.Icon = App.CreateMenuIcon("Icons.Action");
|
|
item.Header = label;
|
|
item.Click += async (_, e) =>
|
|
{
|
|
await repo.ExecCustomActionAsync(dup, target);
|
|
e.Handled = true;
|
|
};
|
|
|
|
custom.Items.Add(item);
|
|
}
|
|
|
|
menu.Items.Add(custom);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
}
|
|
}
|