using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { public partial class CommitDetail : ObservableObject { public DiffContext DiffContext { get => _diffContext; private set => SetProperty(ref _diffContext, value); } public int ActivePageIndex { get => _activePageIndex; set => SetProperty(ref _activePageIndex, value); } public Models.Commit Commit { get => _commit; set { if (SetProperty(ref _commit, value)) Refresh(); } } public string FullMessage { get => _fullMessage; private set => SetProperty(ref _fullMessage, value); } public List Changes { get => _changes; set => SetProperty(ref _changes, value); } public List VisibleChanges { get => _visibleChanges; set => SetProperty(ref _visibleChanges, value); } public List SelectedChanges { get => _selectedChanges; set { if (SetProperty(ref _selectedChanges, value)) { if (value == null || value.Count != 1) DiffContext = null; else DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value[0]), _diffContext); } } } public string SearchChangeFilter { get => _searchChangeFilter; set { if (SetProperty(ref _searchChangeFilter, value)) { RefreshVisibleChanges(); } } } public object ViewRevisionFileContent { get => _viewRevisionFileContent; set => SetProperty(ref _viewRevisionFileContent, value); } public CommitDetail(string repo) { _repo = repo; } public void Cleanup() { _repo = null; _commit = null; if (_changes != null) _changes.Clear(); if (_visibleChanges != null) _visibleChanges.Clear(); if (_selectedChanges != null) _selectedChanges.Clear(); _searchChangeFilter = null; _diffContext = null; _viewRevisionFileContent = null; _cancelToken = null; } public void NavigateTo(string commitSHA) { var repo = App.FindOpenedRepository(_repo); repo?.NavigateToCommit(commitSHA); } public void ClearSearchChangeFilter() { SearchChangeFilter = string.Empty; } public List GetRevisionFilesUnderFolder(string parentFolder) { return new Commands.QueryRevisionObjects(_repo, _commit.SHA, parentFolder).Result(); } public void ViewRevisionFile(Models.Object file) { if (file == null) { ViewRevisionFileContent = null; return; } switch (file.Type) { case Models.ObjectType.Blob: Task.Run(() => { var isBinary = new Commands.IsBinary(_repo, _commit.SHA, file.Path).Result(); if (isBinary) { var ext = Path.GetExtension(file.Path); if (IMG_EXTS.Contains(ext)) { var stream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path); var bitmap = stream.Length > 0 ? new Bitmap(stream) : null; Dispatcher.UIThread.Invoke(() => { ViewRevisionFileContent = new Models.RevisionImageFile() { Image = bitmap }; }); } else { var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result(); Dispatcher.UIThread.Invoke(() => { ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size }; }); } return; } var contentStream = Commands.QueryFileContent.Run(_repo, _commit.SHA, file.Path); var content = new StreamReader(contentStream).ReadToEnd(); var matchLFS = REG_LFS_FORMAT().Match(content); if (matchLFS.Success) { var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() }; obj.Object.Oid = matchLFS.Groups[1].Value; obj.Object.Size = long.Parse(matchLFS.Groups[2].Value); Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = obj); } else { var txt = new Models.RevisionTextFile() { Content = content }; Dispatcher.UIThread.Invoke(() => ViewRevisionFileContent = txt); } }); break; case Models.ObjectType.Commit: Task.Run(() => { var submoduleRoot = Path.Combine(_repo, file.Path); var commit = new Commands.QuerySingleCommit(submoduleRoot, file.SHA).Result(); if (commit != null) { var body = new Commands.QueryCommitFullMessage(submoduleRoot, file.SHA).Result(); Dispatcher.UIThread.Invoke(() => { ViewRevisionFileContent = new Models.RevisionSubmodule() { Commit = commit, FullMessage = body, }; }); } else { Dispatcher.UIThread.Invoke(() => { ViewRevisionFileContent = new Models.RevisionSubmodule() { Commit = new Models.Commit() { SHA = file.SHA }, FullMessage = string.Empty, }; }); } }); break; default: ViewRevisionFileContent = null; break; } } public ContextMenu CreateChangeContextMenu(Models.Change change) { var menu = new ContextMenu(); var diffWithMerger = new MenuItem(); diffWithMerger.Header = App.Text("DiffWithMerger"); diffWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); diffWithMerger.Click += (_, ev) => { var toolType = Preference.Instance.ExternalMergeToolType; var toolPath = Preference.Instance.ExternalMergeToolPath; var opt = new Models.DiffOption(_commit, change); Task.Run(() => Commands.MergeTool.OpenForDiff(_repo, toolType, toolPath, opt)); ev.Handled = true; }; menu.Items.Add(diffWithMerger); if (change.Index != Models.ChangeState.Deleted) { var history = new MenuItem(); history.Header = App.Text("FileHistory"); history.Icon = App.CreateMenuIcon("Icons.Histories"); history.Click += (_, ev) => { var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) }; window.Show(); ev.Handled = true; }; var blame = new MenuItem(); blame.Header = App.Text("Blame"); blame.Icon = App.CreateMenuIcon("Icons.Blame"); blame.Click += (_, ev) => { var window = new Views.Blame() { DataContext = new Blame(_repo, change.Path, _commit.SHA) }; window.Show(); ev.Handled = true; }; var full = Path.GetFullPath(Path.Combine(_repo, change.Path)); var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); explore.Icon = App.CreateMenuIcon("Icons.Explore"); explore.IsEnabled = File.Exists(full); explore.Click += (_, ev) => { Native.OS.OpenInFileManager(full, true); ev.Handled = true; }; menu.Items.Add(new MenuItem { Header = "-" }); menu.Items.Add(history); menu.Items.Add(blame); menu.Items.Add(explore); menu.Items.Add(new MenuItem { Header = "-" }); } 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; }; menu.Items.Add(copyPath); var copyFileName = new MenuItem(); copyFileName.Header = App.Text("CopyFileName"); copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); copyFileName.Click += (_, e) => { App.CopyText(Path.GetFileName(change.Path)); e.Handled = true; }; menu.Items.Add(copyFileName); return menu; } public ContextMenu CreateRevisionFileContextMenu(Models.Object file) { var history = new MenuItem(); history.Header = App.Text("FileHistory"); history.Icon = App.CreateMenuIcon("Icons.Histories"); history.Click += (_, ev) => { var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path) }; window.Show(); 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) => { var window = new Views.Blame() { DataContext = new Blame(_repo, file.Path, _commit.SHA) }; window.Show(); ev.Handled = true; }; var full = Path.GetFullPath(Path.Combine(_repo, file.Path)); var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); explore.Icon = App.CreateMenuIcon("Icons.Explore"); explore.Click += (_, ev) => { Native.OS.OpenInFileManager(full, file.Type == Models.ObjectType.Blob); 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 topLevel = App.GetTopLevel(); if (topLevel == null) return; var options = new FolderPickerOpenOptions() { AllowMultiple = false }; var selected = await topLevel.StorageProvider.OpenFolderPickerAsync(options); if (selected.Count == 1) { var saveTo = Path.Combine(selected[0].Path.LocalPath, Path.GetFileName(file.Path)); Commands.SaveRevisionFile.Run(_repo, _commit.SHA, file.Path, saveTo); } ev.Handled = true; }; 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 copyFileName = new MenuItem(); copyFileName.Header = App.Text("CopyFileName"); copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); copyFileName.Click += (_, e) => { App.CopyText(Path.GetFileName(file.Path)); e.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(history); menu.Items.Add(blame); menu.Items.Add(explore); menu.Items.Add(saveAs); menu.Items.Add(copyPath); menu.Items.Add(copyFileName); return menu; } private void Refresh() { _changes = null; FullMessage = string.Empty; VisibleChanges = null; SelectedChanges = null; ViewRevisionFileContent = null; if (_commit == null) return; if (_cancelToken != null) _cancelToken.Requested = true; _cancelToken = new Commands.Command.CancelToken(); Task.Run(() => { var fullMessage = new Commands.QueryCommitFullMessage(_repo, _commit.SHA).Result(); var parent = _commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : _commit.Parents[0]; var cmdChanges = new Commands.CompareRevisions(_repo, parent, _commit.SHA) { Cancel = _cancelToken }; var changes = cmdChanges.Result(); var visible = changes; if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) { visible = new List(); foreach (var c in changes) { if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) visible.Add(c); } } if (!cmdChanges.Cancel.Requested) { Dispatcher.UIThread.Post(() => { FullMessage = fullMessage; Changes = changes; VisibleChanges = visible; }); } }); } private void RefreshVisibleChanges() { if (_changes == null) return; if (string.IsNullOrEmpty(_searchChangeFilter)) { VisibleChanges = _changes; } else { var visible = new List(); foreach (var c in _changes) { if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) visible.Add(c); } VisibleChanges = visible; } } [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")] private static partial Regex REG_LFS_FORMAT(); private static readonly HashSet IMG_EXTS = new HashSet() { ".ico", ".bmp", ".jpg", ".png", ".jpeg" }; private string _repo; private int _activePageIndex = 0; private Models.Commit _commit = null; private string _fullMessage = string.Empty; private List _changes = null; private List _visibleChanges = null; private List _selectedChanges = null; private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; private object _viewRevisionFileContent = null; private Commands.Command.CancelToken _cancelToken = null; } }