mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-06-15 23:54:58 +00:00

Instead of checking intersections of inline elements yourself before adding an inline element, the new class `InlineElementCollector` prevents intersections internally. Additionally the inline elements are sorted by the new class, so it's no longer necessary to do this after adding the inline elements.
886 lines
33 KiB
C#
886 lines
33 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using Avalonia.Controls;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Threading;
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
namespace SourceGit.ViewModels
|
|
{
|
|
public partial class CommitDetail : ObservableObject, IDisposable
|
|
{
|
|
public int ActivePageIndex
|
|
{
|
|
get => _repo.CommitDetailActivePageIndex;
|
|
set
|
|
{
|
|
if (_repo.CommitDetailActivePageIndex != value)
|
|
{
|
|
_repo.CommitDetailActivePageIndex = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public Models.Commit Commit
|
|
{
|
|
get => _commit;
|
|
set
|
|
{
|
|
if (SetProperty(ref _commit, value))
|
|
Refresh();
|
|
}
|
|
}
|
|
|
|
public Models.CommitFullMessage FullMessage
|
|
{
|
|
get => _fullMessage;
|
|
private set => SetProperty(ref _fullMessage, value);
|
|
}
|
|
|
|
public Models.CommitSignInfo SignInfo
|
|
{
|
|
get => _signInfo;
|
|
private set => SetProperty(ref _signInfo, value);
|
|
}
|
|
|
|
public List<Models.CommitLink> WebLinks
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public List<string> Children
|
|
{
|
|
get => _children;
|
|
private set => SetProperty(ref _children, value);
|
|
}
|
|
|
|
public List<Models.Change> Changes
|
|
{
|
|
get => _changes;
|
|
set => SetProperty(ref _changes, value);
|
|
}
|
|
|
|
public List<Models.Change> VisibleChanges
|
|
{
|
|
get => _visibleChanges;
|
|
set => SetProperty(ref _visibleChanges, value);
|
|
}
|
|
|
|
public List<Models.Change> SelectedChanges
|
|
{
|
|
get => _selectedChanges;
|
|
set
|
|
{
|
|
if (SetProperty(ref _selectedChanges, value))
|
|
{
|
|
if (value == null || value.Count != 1)
|
|
DiffContext = null;
|
|
else
|
|
DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_commit, value[0]), _diffContext);
|
|
}
|
|
}
|
|
}
|
|
|
|
public DiffContext DiffContext
|
|
{
|
|
get => _diffContext;
|
|
private set => SetProperty(ref _diffContext, value);
|
|
}
|
|
|
|
public string SearchChangeFilter
|
|
{
|
|
get => _searchChangeFilter;
|
|
set
|
|
{
|
|
if (SetProperty(ref _searchChangeFilter, value))
|
|
RefreshVisibleChanges();
|
|
}
|
|
}
|
|
|
|
public string ViewRevisionFilePath
|
|
{
|
|
get => _viewRevisionFilePath;
|
|
private set => SetProperty(ref _viewRevisionFilePath, value);
|
|
}
|
|
|
|
public object ViewRevisionFileContent
|
|
{
|
|
get => _viewRevisionFileContent;
|
|
private set => SetProperty(ref _viewRevisionFileContent, value);
|
|
}
|
|
|
|
public string RevisionFileSearchFilter
|
|
{
|
|
get => _revisionFileSearchFilter;
|
|
set
|
|
{
|
|
if (SetProperty(ref _revisionFileSearchFilter, value))
|
|
RefreshRevisionSearchSuggestion();
|
|
}
|
|
}
|
|
|
|
public List<string> RevisionFileSearchSuggestion
|
|
{
|
|
get => _revisionFileSearchSuggestion;
|
|
private set => SetProperty(ref _revisionFileSearchSuggestion, value);
|
|
}
|
|
|
|
public CommitDetail(Repository repo)
|
|
{
|
|
_repo = repo;
|
|
WebLinks = Models.CommitLink.Get(repo.Remotes);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_repo = null;
|
|
_commit = null;
|
|
_changes = null;
|
|
_visibleChanges = null;
|
|
_selectedChanges = null;
|
|
_signInfo = null;
|
|
_searchChangeFilter = null;
|
|
_diffContext = null;
|
|
_viewRevisionFileContent = null;
|
|
_cancellationSource = null;
|
|
_requestingRevisionFiles = false;
|
|
_revisionFiles = null;
|
|
_revisionFileSearchSuggestion = null;
|
|
}
|
|
|
|
public void NavigateTo(string commitSHA)
|
|
{
|
|
_repo?.NavigateToCommit(commitSHA);
|
|
}
|
|
|
|
public List<Models.Decorator> GetRefsContainsThisCommit()
|
|
{
|
|
return new Commands.QueryRefsContainsCommit(_repo.FullPath, _commit.SHA).Result();
|
|
}
|
|
|
|
public void ClearSearchChangeFilter()
|
|
{
|
|
SearchChangeFilter = string.Empty;
|
|
}
|
|
|
|
public void ClearRevisionFileSearchFilter()
|
|
{
|
|
RevisionFileSearchFilter = string.Empty;
|
|
}
|
|
|
|
public void CancelRevisionFileSuggestions()
|
|
{
|
|
RevisionFileSearchSuggestion = null;
|
|
}
|
|
|
|
public Models.Commit GetParent(string sha)
|
|
{
|
|
return new Commands.QuerySingleCommit(_repo.FullPath, sha).Result();
|
|
}
|
|
|
|
public List<Models.Object> GetRevisionFilesUnderFolder(string parentFolder)
|
|
{
|
|
return new Commands.QueryRevisionObjects(_repo.FullPath, _commit.SHA, parentFolder).Result();
|
|
}
|
|
|
|
public void ViewRevisionFile(Models.Object file)
|
|
{
|
|
if (file == null)
|
|
{
|
|
ViewRevisionFilePath = string.Empty;
|
|
ViewRevisionFileContent = null;
|
|
return;
|
|
}
|
|
|
|
ViewRevisionFilePath = file.Path;
|
|
|
|
switch (file.Type)
|
|
{
|
|
case Models.ObjectType.Blob:
|
|
Task.Run(() =>
|
|
{
|
|
var isBinary = new Commands.IsBinary(_repo.FullPath, _commit.SHA, file.Path).Result();
|
|
if (isBinary)
|
|
{
|
|
var imgDecoder = ImageSource.GetDecoder(file.Path);
|
|
if (imgDecoder != Models.ImageDecoder.None)
|
|
{
|
|
var source = ImageSource.FromRevision(_repo.FullPath, _commit.SHA, file.Path, imgDecoder);
|
|
var image = new Models.RevisionImageFile(file.Path, source.Bitmap, source.Size);
|
|
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = image);
|
|
}
|
|
else
|
|
{
|
|
var size = new Commands.QueryFileSize(_repo.FullPath, file.Path, _commit.SHA).Result();
|
|
var binary = new Models.RevisionBinaryFile() { Size = size };
|
|
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = binary);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _commit.SHA, file.Path);
|
|
var content = new StreamReader(contentStream).ReadToEnd();
|
|
var lfs = Models.LFSObject.Parse(content);
|
|
if (lfs != null)
|
|
{
|
|
var imgDecoder = ImageSource.GetDecoder(file.Path);
|
|
if (imgDecoder != Models.ImageDecoder.None)
|
|
{
|
|
var combined = new RevisionLFSImage(_repo.FullPath, file.Path, lfs, imgDecoder);
|
|
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = combined);
|
|
}
|
|
else
|
|
{
|
|
var rlfs = new Models.RevisionLFSObject() { Object = lfs };
|
|
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = rlfs);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var txt = new Models.RevisionTextFile() { FileName = file.Path, Content = content };
|
|
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = txt);
|
|
}
|
|
});
|
|
break;
|
|
case Models.ObjectType.Commit:
|
|
Task.Run(() =>
|
|
{
|
|
var submoduleRoot = Path.Combine(_repo.FullPath, file.Path).Replace('\\', '/').Trim('/');
|
|
var commit = new Commands.QuerySingleCommit(submoduleRoot, file.SHA).Result();
|
|
var message = commit != null ? new Commands.QueryCommitFullMessage(submoduleRoot, file.SHA).Result() : null;
|
|
var module = new Models.RevisionSubmodule()
|
|
{
|
|
Commit = commit ?? new Models.Commit() { SHA = _commit.SHA },
|
|
FullMessage = new Models.CommitFullMessage { Message = message }
|
|
};
|
|
|
|
Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = module);
|
|
});
|
|
break;
|
|
default:
|
|
ViewRevisionFileContent = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
public ContextMenu CreateChangeContextMenu(Models.Change change)
|
|
{
|
|
var diffWithMerger = new MenuItem();
|
|
diffWithMerger.Header = App.Text("DiffWithMerger");
|
|
diffWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
|
|
diffWithMerger.Click += (_, ev) =>
|
|
{
|
|
var toolType = Preferences.Instance.ExternalMergeToolType;
|
|
var toolPath = Preferences.Instance.ExternalMergeToolPath;
|
|
var opt = new Models.DiffOption(_commit, change);
|
|
|
|
Task.Run(() => Commands.MergeTool.OpenForDiff(_repo.FullPath, toolType, toolPath, opt));
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var fullPath = Native.OS.GetAbsPath(_repo.FullPath, change.Path);
|
|
var explore = new MenuItem();
|
|
explore.Header = App.Text("RevealFile");
|
|
explore.Icon = App.CreateMenuIcon("Icons.Explore");
|
|
explore.IsEnabled = File.Exists(fullPath);
|
|
explore.Click += (_, ev) =>
|
|
{
|
|
Native.OS.OpenInFileManager(fullPath, true);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("FileHistory");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, ev) =>
|
|
{
|
|
App.ShowWindow(new FileHistories(_repo, change.Path, _commit.SHA), false);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var blame = new MenuItem();
|
|
blame.Header = App.Text("Blame");
|
|
blame.Icon = App.CreateMenuIcon("Icons.Blame");
|
|
blame.IsEnabled = change.Index != Models.ChangeState.Deleted;
|
|
blame.Click += (_, ev) =>
|
|
{
|
|
App.ShowWindow(new Blame(_repo.FullPath, change.Path, _commit.SHA), false);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var patch = new MenuItem();
|
|
patch.Header = App.Text("FileCM.SaveAsPatch");
|
|
patch.Icon = App.CreateMenuIcon("Icons.Diff");
|
|
patch.Click += async (_, e) =>
|
|
{
|
|
var storageProvider = App.GetStorageProvider();
|
|
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"] }];
|
|
|
|
var baseRevision = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : _commit.Parents[0];
|
|
var storageFile = await storageProvider.SaveFilePickerAsync(options);
|
|
if (storageFile != null)
|
|
{
|
|
var saveTo = storageFile.Path.LocalPath;
|
|
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessRevisionCompareChanges(_repo.FullPath, [change], baseRevision, _commit.SHA, saveTo));
|
|
if (succ)
|
|
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
|
|
}
|
|
|
|
e.Handled = true;
|
|
};
|
|
|
|
var menu = new ContextMenu();
|
|
menu.Items.Add(diffWithMerger);
|
|
menu.Items.Add(explore);
|
|
menu.Items.Add(new MenuItem { Header = "-" });
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(blame);
|
|
menu.Items.Add(patch);
|
|
menu.Items.Add(new MenuItem { Header = "-" });
|
|
|
|
if (!_repo.IsBare)
|
|
{
|
|
var resetToThisRevision = new MenuItem();
|
|
resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision");
|
|
resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout");
|
|
resetToThisRevision.Click += async (_, ev) =>
|
|
{
|
|
await ResetToThisRevision(change.Path);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var resetToFirstParent = new MenuItem();
|
|
resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision");
|
|
resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout");
|
|
resetToFirstParent.IsEnabled = _commit.Parents.Count > 0;
|
|
resetToFirstParent.Click += async (_, ev) =>
|
|
{
|
|
await ResetToParentRevision(change);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(resetToThisRevision);
|
|
menu.Items.Add(resetToFirstParent);
|
|
menu.Items.Add(new MenuItem { Header = "-" });
|
|
|
|
TryToAddContextMenuItemsForGitLFS(menu, fullPath, change.Path);
|
|
}
|
|
|
|
var copyPath = new MenuItem();
|
|
copyPath.Header = App.Text("CopyPath");
|
|
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyPath.Click += (_, ev) =>
|
|
{
|
|
App.CopyText(change.Path);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var copyFullPath = new MenuItem();
|
|
copyFullPath.Header = App.Text("CopyFullPath");
|
|
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyFullPath.Click += (_, e) =>
|
|
{
|
|
App.CopyText(fullPath);
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(copyPath);
|
|
menu.Items.Add(copyFullPath);
|
|
return menu;
|
|
}
|
|
|
|
public ContextMenu CreateRevisionFileContextMenu(Models.Object file)
|
|
{
|
|
var menu = new ContextMenu();
|
|
var fullPath = Native.OS.GetAbsPath(_repo.FullPath, file.Path);
|
|
var explore = new MenuItem();
|
|
explore.Header = App.Text("RevealFile");
|
|
explore.Icon = App.CreateMenuIcon("Icons.Explore");
|
|
explore.IsEnabled = File.Exists(fullPath);
|
|
explore.Click += (_, ev) =>
|
|
{
|
|
Native.OS.OpenInFileManager(fullPath, file.Type == Models.ObjectType.Blob);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var openWith = new MenuItem();
|
|
openWith.Header = App.Text("OpenWith");
|
|
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
|
|
openWith.Click += async (_, ev) =>
|
|
{
|
|
var fileName = Path.GetFileNameWithoutExtension(fullPath) ?? "";
|
|
var fileExt = Path.GetExtension(fullPath) ?? "";
|
|
var tmpFile = Path.Combine(Path.GetTempPath(), $"{fileName}~{_commit.SHA.Substring(0, 10)}{fileExt}");
|
|
await Task.Run(() => Commands.SaveRevisionFile.Run(_repo.FullPath, _commit.SHA, file.Path, tmpFile));
|
|
Native.OS.OpenWithDefaultEditor(tmpFile);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var saveAs = new MenuItem();
|
|
saveAs.Header = App.Text("SaveAs");
|
|
saveAs.Icon = App.CreateMenuIcon("Icons.Save");
|
|
saveAs.IsEnabled = file.Type == Models.ObjectType.Blob;
|
|
saveAs.Click += async (_, ev) =>
|
|
{
|
|
var storageProvider = App.GetStorageProvider();
|
|
if (storageProvider == null)
|
|
return;
|
|
|
|
var options = new FolderPickerOpenOptions() { AllowMultiple = false };
|
|
try
|
|
{
|
|
var selected = await storageProvider.OpenFolderPickerAsync(options);
|
|
if (selected.Count == 1)
|
|
{
|
|
var folder = selected[0];
|
|
var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString();
|
|
var saveTo = Path.Combine(folderPath, Path.GetFileName(file.Path)!);
|
|
await Task.Run(() => Commands.SaveRevisionFile.Run(_repo.FullPath, _commit.SHA, file.Path, saveTo));
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
App.RaiseException(_repo.FullPath, $"Failed to save file: {e.Message}");
|
|
}
|
|
|
|
ev.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(explore);
|
|
menu.Items.Add(openWith);
|
|
menu.Items.Add(saveAs);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
|
|
var history = new MenuItem();
|
|
history.Header = App.Text("FileHistory");
|
|
history.Icon = App.CreateMenuIcon("Icons.Histories");
|
|
history.Click += (_, ev) =>
|
|
{
|
|
App.ShowWindow(new FileHistories(_repo, file.Path, _commit.SHA), false);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var blame = new MenuItem();
|
|
blame.Header = App.Text("Blame");
|
|
blame.Icon = App.CreateMenuIcon("Icons.Blame");
|
|
blame.IsEnabled = file.Type == Models.ObjectType.Blob;
|
|
blame.Click += (_, ev) =>
|
|
{
|
|
App.ShowWindow(new Blame(_repo.FullPath, file.Path, _commit.SHA), false);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(history);
|
|
menu.Items.Add(blame);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
|
|
if (!_repo.IsBare)
|
|
{
|
|
var resetToThisRevision = new MenuItem();
|
|
resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision");
|
|
resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout");
|
|
resetToThisRevision.Click += async (_, ev) =>
|
|
{
|
|
await ResetToThisRevision(file.Path);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var change = _changes.Find(x => x.Path == file.Path) ?? new Models.Change() { Index = Models.ChangeState.None, Path = file.Path };
|
|
var resetToFirstParent = new MenuItem();
|
|
resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision");
|
|
resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout");
|
|
resetToFirstParent.IsEnabled = _commit.Parents.Count > 0;
|
|
resetToFirstParent.Click += async (_, ev) =>
|
|
{
|
|
await ResetToParentRevision(change);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(resetToThisRevision);
|
|
menu.Items.Add(resetToFirstParent);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
|
|
TryToAddContextMenuItemsForGitLFS(menu, fullPath, file.Path);
|
|
}
|
|
|
|
var copyPath = new MenuItem();
|
|
copyPath.Header = App.Text("CopyPath");
|
|
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyPath.Click += (_, ev) =>
|
|
{
|
|
App.CopyText(file.Path);
|
|
ev.Handled = true;
|
|
};
|
|
|
|
var copyFullPath = new MenuItem();
|
|
copyFullPath.Header = App.Text("CopyFullPath");
|
|
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
|
copyFullPath.Click += (_, e) =>
|
|
{
|
|
App.CopyText(fullPath);
|
|
e.Handled = true;
|
|
};
|
|
|
|
menu.Items.Add(copyPath);
|
|
menu.Items.Add(copyFullPath);
|
|
return menu;
|
|
}
|
|
|
|
private void Refresh()
|
|
{
|
|
_changes = null;
|
|
_requestingRevisionFiles = false;
|
|
_revisionFiles = null;
|
|
|
|
SignInfo = null;
|
|
ViewRevisionFileContent = null;
|
|
Children = null;
|
|
RevisionFileSearchFilter = string.Empty;
|
|
RevisionFileSearchSuggestion = null;
|
|
|
|
if (_commit == null)
|
|
return;
|
|
|
|
if (_cancellationSource is { IsCancellationRequested: false })
|
|
_cancellationSource.Cancel();
|
|
|
|
_cancellationSource = new CancellationTokenSource();
|
|
var token = _cancellationSource.Token;
|
|
|
|
Task.Run(() =>
|
|
{
|
|
var message = new Commands.QueryCommitFullMessage(_repo.FullPath, _commit.SHA).Result();
|
|
var inlines = ParseInlinesInMessage(message);
|
|
|
|
if (!token.IsCancellationRequested)
|
|
Dispatcher.UIThread.Invoke(() => FullMessage = new Models.CommitFullMessage { Message = message, Inlines = inlines });
|
|
});
|
|
|
|
Task.Run(() =>
|
|
{
|
|
var signInfo = new Commands.QueryCommitSignInfo(_repo.FullPath, _commit.SHA, !_repo.HasAllowedSignersFile).Result();
|
|
if (!token.IsCancellationRequested)
|
|
Dispatcher.UIThread.Invoke(() => SignInfo = signInfo);
|
|
});
|
|
|
|
if (Preferences.Instance.ShowChildren)
|
|
{
|
|
Task.Run(() =>
|
|
{
|
|
var max = Preferences.Instance.MaxHistoryCommits;
|
|
var cmd = new Commands.QueryCommitChildren(_repo.FullPath, _commit.SHA, max) { CancellationToken = token };
|
|
var children = cmd.Result();
|
|
if (!token.IsCancellationRequested)
|
|
Dispatcher.UIThread.Post(() => Children = children);
|
|
});
|
|
}
|
|
|
|
Task.Run(() =>
|
|
{
|
|
var parent = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : _commit.Parents[0];
|
|
var cmd = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { CancellationToken = token };
|
|
var changes = cmd.Result();
|
|
var visible = changes;
|
|
if (!string.IsNullOrWhiteSpace(_searchChangeFilter))
|
|
{
|
|
visible = new List<Models.Change>();
|
|
foreach (var c in changes)
|
|
{
|
|
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase))
|
|
visible.Add(c);
|
|
}
|
|
}
|
|
|
|
if (!token.IsCancellationRequested)
|
|
{
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
Changes = changes;
|
|
VisibleChanges = visible;
|
|
|
|
if (visible.Count == 0)
|
|
SelectedChanges = null;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private Models.InlineElementCollector ParseInlinesInMessage(string message)
|
|
{
|
|
var inlines = new Models.InlineElementCollector();
|
|
if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules)
|
|
{
|
|
foreach (var rule in rules)
|
|
rule.Matches(inlines, message);
|
|
}
|
|
|
|
var urlMatches = REG_URL_FORMAT().Matches(message);
|
|
for (int i = 0; i < urlMatches.Count; i++)
|
|
{
|
|
var match = urlMatches[i];
|
|
if (!match.Success)
|
|
continue;
|
|
|
|
var start = match.Index;
|
|
var len = match.Length;
|
|
|
|
var url = message.Substring(start, len);
|
|
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
|
inlines.Add(new Models.InlineElement(Models.InlineElementType.Link, start, len, url));
|
|
}
|
|
|
|
var shaMatches = REG_SHA_FORMAT().Matches(message);
|
|
for (int i = 0; i < shaMatches.Count; i++)
|
|
{
|
|
var match = shaMatches[i];
|
|
if (!match.Success)
|
|
continue;
|
|
|
|
var start = match.Index;
|
|
var len = match.Length;
|
|
|
|
var sha = match.Groups[1].Value;
|
|
var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result();
|
|
if (isCommitSHA)
|
|
inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha));
|
|
}
|
|
|
|
return inlines;
|
|
}
|
|
|
|
private void RefreshVisibleChanges()
|
|
{
|
|
if (_changes == null)
|
|
return;
|
|
|
|
if (string.IsNullOrEmpty(_searchChangeFilter))
|
|
{
|
|
VisibleChanges = _changes;
|
|
}
|
|
else
|
|
{
|
|
var visible = new List<Models.Change>();
|
|
foreach (var c in _changes)
|
|
{
|
|
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase))
|
|
visible.Add(c);
|
|
}
|
|
|
|
VisibleChanges = visible;
|
|
}
|
|
}
|
|
|
|
private void TryToAddContextMenuItemsForGitLFS(ContextMenu menu, string fullPath, string path)
|
|
{
|
|
if (_repo.Remotes.Count == 0 || !File.Exists(fullPath))
|
|
return;
|
|
|
|
var lfsEnabled = new Commands.LFS(_repo.FullPath).IsEnabled();
|
|
if (!lfsEnabled)
|
|
return;
|
|
|
|
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");
|
|
if (_repo.Remotes.Count == 1)
|
|
{
|
|
lfsLock.Click += async (_, e) =>
|
|
{
|
|
var log = _repo.CreateLog("Lock LFS file");
|
|
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(_repo.Remotes[0].Name, path, log));
|
|
if (succ)
|
|
App.SendNotification(_repo.FullPath, $"Lock file \"{path}\" successfully!");
|
|
|
|
log.Complete();
|
|
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) =>
|
|
{
|
|
var log = _repo.CreateLog("Lock LFS file");
|
|
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(remoteName, path, log));
|
|
if (succ)
|
|
App.SendNotification(_repo.FullPath, $"Lock file \"{path}\" successfully!");
|
|
|
|
log.Complete();
|
|
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");
|
|
if (_repo.Remotes.Count == 1)
|
|
{
|
|
lfsUnlock.Click += async (_, e) =>
|
|
{
|
|
var log = _repo.CreateLog("Unlock LFS file");
|
|
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(_repo.Remotes[0].Name, path, false, log));
|
|
if (succ)
|
|
App.SendNotification(_repo.FullPath, $"Unlock file \"{path}\" successfully!");
|
|
|
|
log.Complete();
|
|
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) =>
|
|
{
|
|
var log = _repo.CreateLog("Unlock LFS file");
|
|
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(remoteName, path, false, log));
|
|
if (succ)
|
|
App.SendNotification(_repo.FullPath, $"Unlock file \"{path}\" successfully!");
|
|
|
|
log.Complete();
|
|
e.Handled = true;
|
|
};
|
|
lfsUnlock.Items.Add(unlockRemote);
|
|
}
|
|
}
|
|
lfs.Items.Add(lfsUnlock);
|
|
|
|
menu.Items.Add(lfs);
|
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
|
}
|
|
|
|
private void RefreshRevisionSearchSuggestion()
|
|
{
|
|
if (!string.IsNullOrEmpty(_revisionFileSearchFilter))
|
|
{
|
|
if (_revisionFiles == null)
|
|
{
|
|
if (_requestingRevisionFiles)
|
|
return;
|
|
|
|
var sha = Commit.SHA;
|
|
_requestingRevisionFiles = true;
|
|
|
|
Task.Run(() =>
|
|
{
|
|
var files = new Commands.QueryRevisionFileNames(_repo.FullPath, sha).Result();
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
if (sha == Commit.SHA && _requestingRevisionFiles)
|
|
{
|
|
_revisionFiles = files;
|
|
_requestingRevisionFiles = false;
|
|
|
|
if (!string.IsNullOrEmpty(_revisionFileSearchFilter))
|
|
CalcRevisionFileSearchSuggestion();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
else
|
|
{
|
|
CalcRevisionFileSearchSuggestion();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RevisionFileSearchSuggestion = null;
|
|
GC.Collect();
|
|
}
|
|
}
|
|
|
|
private void CalcRevisionFileSearchSuggestion()
|
|
{
|
|
var suggestion = new List<string>();
|
|
foreach (var file in _revisionFiles)
|
|
{
|
|
if (file.Contains(_revisionFileSearchFilter, StringComparison.OrdinalIgnoreCase) &&
|
|
file.Length != _revisionFileSearchFilter.Length)
|
|
suggestion.Add(file);
|
|
|
|
if (suggestion.Count >= 100)
|
|
break;
|
|
}
|
|
|
|
RevisionFileSearchSuggestion = suggestion;
|
|
}
|
|
|
|
private Task ResetToThisRevision(string path)
|
|
{
|
|
var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'");
|
|
|
|
return Task.Run(() =>
|
|
{
|
|
new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(path, $"{_commit.SHA}");
|
|
log.Complete();
|
|
});
|
|
}
|
|
|
|
private Task ResetToParentRevision(Models.Change change)
|
|
{
|
|
var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'");
|
|
|
|
return Task.Run(() =>
|
|
{
|
|
if (change.Index == Models.ChangeState.Renamed)
|
|
new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(change.OriginalPath, $"{_commit.SHA}~1");
|
|
|
|
new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevision(change.Path, $"{_commit.SHA}~1");
|
|
log.Complete();
|
|
});
|
|
}
|
|
|
|
[GeneratedRegex(@"\b(https?://|ftp://)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]")]
|
|
private static partial Regex REG_URL_FORMAT();
|
|
|
|
[GeneratedRegex(@"\b([0-9a-fA-F]{6,40})\b")]
|
|
private static partial Regex REG_SHA_FORMAT();
|
|
|
|
private Repository _repo = null;
|
|
private Models.Commit _commit = null;
|
|
private Models.CommitFullMessage _fullMessage = null;
|
|
private Models.CommitSignInfo _signInfo = null;
|
|
private List<string> _children = null;
|
|
private List<Models.Change> _changes = null;
|
|
private List<Models.Change> _visibleChanges = null;
|
|
private List<Models.Change> _selectedChanges = null;
|
|
private string _searchChangeFilter = string.Empty;
|
|
private DiffContext _diffContext = null;
|
|
private string _viewRevisionFilePath = string.Empty;
|
|
private object _viewRevisionFileContent = null;
|
|
private CancellationTokenSource _cancellationSource = null;
|
|
private bool _requestingRevisionFiles = false;
|
|
private List<string> _revisionFiles = null;
|
|
private string _revisionFileSearchFilter = string.Empty;
|
|
private List<string> _revisionFileSearchSuggestion = null;
|
|
}
|
|
}
|