feature: supports show commit histories under selected folder (#1470)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2025-07-01 16:03:27 +08:00
parent f62eb88cc8
commit d4723eeea2
12 changed files with 516 additions and 83 deletions

View File

@@ -296,6 +296,85 @@ namespace SourceGit.ViewModels
});
}
public ContextMenu CreateChangeContextMenuByFolder(ChangeTreeNode node, List<Models.Change> changes)
{
var fullPath = Native.OS.GetAbsPath(_repo.FullPath, node.FullPath);
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = Directory.Exists(fullPath);
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(fullPath, true);
ev.Handled = true;
};
var history = new MenuItem();
history.Header = App.Text("DirHistories");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
App.ShowWindow(new DirHistories(_repo, node.FullPath, _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, changes, baseRevision, _commit.SHA, saveTo));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
e.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Click += (_, ev) =>
{
App.CopyText(node.FullPath);
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;
};
var menu = new ContextMenu();
menu.Items.Add(explore);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(history);
menu.Items.Add(patch);
menu.Items.Add(new MenuItem { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
return menu;
}
public ContextMenu CreateChangeContextMenu(Models.Change change)
{
var diffWithMerger = new MenuItem();
@@ -428,8 +507,61 @@ namespace SourceGit.ViewModels
return menu;
}
public ContextMenu CreateRevisionFileContextMenuByFolder(string path)
{
var fullPath = Native.OS.GetAbsPath(_repo.FullPath, path);
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = Directory.Exists(fullPath);
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(fullPath, true);
ev.Handled = true;
};
var history = new MenuItem();
history.Header = App.Text("DirHistories");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) =>
{
App.ShowWindow(new DirHistories(_repo, path, _commit.SHA), false);
ev.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Click += (_, ev) =>
{
App.CopyText(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;
};
var menu = new ContextMenu();
menu.Items.Add(explore);
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;
}
public ContextMenu CreateRevisionFileContextMenu(Models.Object file)
{
if (file.Type == Models.ObjectType.Tree)
return CreateRevisionFileContextMenuByFolder(file.Path);
var menu = new ContextMenu();
var fullPath = Native.OS.GetAbsPath(_repo.FullPath, file.Path);
var explore = new MenuItem();

View File

@@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class DirHistories : ObservableObject
{
public string Title
{
get;
}
public bool IsLoading
{
get => _isLoading;
private set => SetProperty(ref _isLoading, value);
}
public List<Models.Commit> Commits
{
get => _commits;
private set => SetProperty(ref _commits, value);
}
public Models.Commit SelectedCommit
{
get => _selectedCommit;
set
{
if (SetProperty(ref _selectedCommit, value))
Detail.Commit = value;
}
}
public CommitDetail Detail
{
get => _detail;
private set => SetProperty(ref _detail, value);
}
public DirHistories(Repository repo, string dir, string revision = null)
{
if (!string.IsNullOrEmpty(revision))
Title = $"{dir} @ {revision}";
else
Title = dir;
_repo = repo;
_detail = new CommitDetail(repo);
Task.Run(() =>
{
var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order -n 10000 {revision??string.Empty} -- \"{dir}\"", false).Result();
Dispatcher.UIThread.Invoke(() =>
{
Commits = commits;
IsLoading = false;
if (commits.Count > 0)
SelectedCommit = commits[0];
});
});
}
public void NavigateToCommit(Models.Commit commit)
{
_repo.NavigateToCommit(commit.SHA);
}
public string GetCommitFullMessage(Models.Commit commit)
{
var sha = commit.SHA;
if (_cachedCommitFullMessage.TryGetValue(sha, out var msg))
return msg;
msg = new Commands.QueryCommitFullMessage(_repo.FullPath, sha).Result();
_cachedCommitFullMessage[sha] = msg;
return msg;
}
private Repository _repo = null;
private bool _isLoading = true;
private List<Models.Commit> _commits = [];
private Models.Commit _selectedCommit = null;
private CommitDetail _detail = null;
private Dictionary<string, string> _cachedCommitFullMessage = new();
}
}

View File

@@ -238,6 +238,11 @@ namespace SourceGit.ViewModels
public class FileHistories : ObservableObject
{
public string Title
{
get;
}
public bool IsLoading
{
get => _isLoading;
@@ -264,6 +269,11 @@ namespace SourceGit.ViewModels
public FileHistories(Repository repo, string file, string commit = null)
{
if (!string.IsNullOrEmpty(commit))
Title = $"{file} @ {commit}";
else
Title = file;
_repo = repo;
Task.Run(() =>

View File

@@ -590,6 +590,7 @@ namespace SourceGit.ViewModels
if (_selectedUnstaged == null || _selectedUnstaged.Count == 0)
return null;
var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder);
var menu = new ContextMenu();
if (_selectedUnstaged.Count == 1)
{
@@ -602,11 +603,8 @@ namespace SourceGit.ViewModels
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Click += (_, e) =>
{
if (string.IsNullOrEmpty(selectedSingleFolder))
Native.OS.OpenInFileManager(path, true);
else
Native.OS.OpenInFileManager(Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder), true);
var target = hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path;
Native.OS.OpenInFileManager(target, true);
e.Handled = true;
};
menu.Items.Add(explore);
@@ -747,23 +745,12 @@ namespace SourceGit.ViewModels
e.Handled = true;
};
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
App.ShowWindow(new FileHistories(_repo, change.Path), false);
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 = "-" });
menu.Items.Add(history);
menu.Items.Add(new MenuItem() { Header = "-" });
var extension = Path.GetExtension(change.Path);
var hasExtra = false;
@@ -773,7 +760,7 @@ namespace SourceGit.ViewModels
addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore");
addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore");
if (!string.IsNullOrEmpty(selectedSingleFolder))
if (hasSelectedFolder)
{
var ignoreFolder = new MenuItem();
ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder");
@@ -827,24 +814,21 @@ namespace SourceGit.ViewModels
menu.Items.Add(addToIgnore);
hasExtra = true;
}
else if (!string.IsNullOrEmpty(selectedSingleFolder))
else if (hasSelectedFolder)
{
var addToIgnore = new MenuItem();
addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore");
addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore");
if (!string.IsNullOrEmpty(selectedSingleFolder))
var ignoreFolder = new MenuItem();
ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder");
ignoreFolder.Click += (_, e) =>
{
var ignoreFolder = new MenuItem();
ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder");
ignoreFolder.Click += (_, e) =>
{
if (_repo.CanCreatePopup())
_repo.ShowPopup(new AddToIgnore(_repo, $"{selectedSingleFolder}/"));
e.Handled = true;
};
addToIgnore.Items.Add(ignoreFolder);
}
if (_repo.CanCreatePopup())
_repo.ShowPopup(new AddToIgnore(_repo, $"{selectedSingleFolder}/"));
e.Handled = true;
};
addToIgnore.Items.Add(ignoreFolder);
menu.Items.Add(addToIgnore);
hasExtra = true;
@@ -981,32 +965,40 @@ namespace SourceGit.ViewModels
menu.Items.Add(new MenuItem() { Header = "-" });
}
var history = new MenuItem();
history.Header = App.Text(hasSelectedFolder ? "DirHistories" : "FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
if (hasSelectedFolder)
App.ShowWindow(new DirHistories(_repo, selectedSingleFolder), false);
else
App.ShowWindow(new FileHistories(_repo, change.Path), false);
e.Handled = true;
};
var copy = new MenuItem();
copy.Header = App.Text("CopyPath");
copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (_, e) =>
{
if (string.IsNullOrEmpty(selectedSingleFolder))
App.CopyText(change.Path);
else
App.CopyText(selectedSingleFolder);
App.CopyText(hasSelectedFolder ? selectedSingleFolder : change.Path);
e.Handled = true;
};
menu.Items.Add(copy);
var copyFullPath = new MenuItem();
copyFullPath.Header = App.Text("CopyFullPath");
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Click += (_, e) =>
{
if (string.IsNullOrEmpty(selectedSingleFolder))
App.CopyText(path);
else
App.CopyText(Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder));
App.CopyText(hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path);
e.Handled = true;
};
menu.Items.Add(history);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copy);
menu.Items.Add(copyFullPath);
}
else
@@ -1073,7 +1065,7 @@ namespace SourceGit.ViewModels
return menu;
}
if (!string.IsNullOrEmpty(selectedSingleFolder))
if (hasSelectedFolder)
{
var dir = Path.Combine(_repo.FullPath, selectedSingleFolder);
var explore = new MenuItem();
@@ -1148,7 +1140,7 @@ namespace SourceGit.ViewModels
menu.Items.Add(stash);
menu.Items.Add(patch);
if (!string.IsNullOrEmpty(selectedSingleFolder))
if (hasSelectedFolder)
{
var ignoreFolder = new MenuItem();
ignoreFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InFolder");
@@ -1167,6 +1159,17 @@ namespace SourceGit.ViewModels
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(addToIgnore);
var history = new MenuItem();
history.Header = App.Text("DirHistories");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
App.ShowWindow(new DirHistories(_repo, selectedSingleFolder), false);
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(history);
var copy = new MenuItem();
copy.Header = App.Text("CopyPath");
copy.Icon = App.CreateMenuIcon("Icons.Copy");
@@ -1235,6 +1238,7 @@ namespace SourceGit.ViewModels
}
}
var hasSelectedFolder = !string.IsNullOrEmpty(selectedSingleFolder);
if (_selectedStaged.Count == 1)
{
var change = _selectedStaged[0];
@@ -1246,11 +1250,8 @@ namespace SourceGit.ViewModels
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.Click += (_, e) =>
{
if (string.IsNullOrEmpty(selectedSingleFolder))
Native.OS.OpenInFileManager(path, true);
else
Native.OS.OpenInFileManager(Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder), true);
var target = hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path;
Native.OS.OpenInFileManager(target, true);
e.Handled = true;
};
@@ -1309,15 +1310,6 @@ namespace SourceGit.ViewModels
e.Handled = true;
};
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
App.ShowWindow(new FileHistories(_repo, change.Path), false);
e.Handled = true;
};
menu.Items.Add(explore);
menu.Items.Add(openWith);
menu.Items.Add(new MenuItem() { Header = "-" });
@@ -1325,8 +1317,6 @@ namespace SourceGit.ViewModels
menu.Items.Add(stash);
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(history);
menu.Items.Add(new MenuItem() { Header = "-" });
var lfsEnabled = new Commands.LFS(_repo.FullPath).IsEnabled();
if (lfsEnabled)
@@ -1423,16 +1413,24 @@ namespace SourceGit.ViewModels
menu.Items.Add(new MenuItem() { Header = "-" });
}
var history = new MenuItem();
history.Header = App.Text(hasSelectedFolder ? "DirHistories" : "FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
if (hasSelectedFolder)
App.ShowWindow(new DirHistories(_repo, selectedSingleFolder), false);
else
App.ShowWindow(new FileHistories(_repo, change.Path), false);
e.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Click += (_, e) =>
{
if (string.IsNullOrEmpty(selectedSingleFolder))
App.CopyText(change.Path);
else
App.CopyText(selectedSingleFolder);
App.CopyText(hasSelectedFolder ? selectedSingleFolder : change.Path);
e.Handled = true;
};
@@ -1441,20 +1439,19 @@ namespace SourceGit.ViewModels
copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyFullPath.Click += (_, e) =>
{
if (string.IsNullOrEmpty(selectedSingleFolder))
App.CopyText(path);
else
App.CopyText(Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder));
var target = hasSelectedFolder ? Native.OS.GetAbsPath(_repo.FullPath, selectedSingleFolder) : path;
App.CopyText(target);
e.Handled = true;
};
menu.Items.Add(history);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copyPath);
menu.Items.Add(copyFullPath);
}
else
{
if (!string.IsNullOrEmpty(selectedSingleFolder))
if (hasSelectedFolder)
{
var dir = Path.Combine(_repo.FullPath, selectedSingleFolder);
var explore = new MenuItem();
@@ -1526,8 +1523,17 @@ namespace SourceGit.ViewModels
menu.Items.Add(ai);
}
if (!string.IsNullOrEmpty(selectedSingleFolder))
if (hasSelectedFolder)
{
var history = new MenuItem();
history.Header = App.Text(hasSelectedFolder ? "DirHistories" : "FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
App.ShowWindow(new DirHistories(_repo, selectedSingleFolder), false);
e.Handled = true;
};
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
@@ -1546,6 +1552,8 @@ namespace SourceGit.ViewModels
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);