style: add .editorconfig for code formatting. see issu #25

This commit is contained in:
leo 2024-03-18 09:37:06 +08:00
parent a8eeea4f78
commit 18aaa0a143
225 changed files with 7781 additions and 3911 deletions

View file

@ -1,42 +1,52 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class AddRemote : Popup {
namespace SourceGit.ViewModels
{
public class AddRemote : Popup
{
[Required(ErrorMessage = "Remote name is required!!!")]
[RegularExpression(@"^[\w\-\.]+$", ErrorMessage = "Bad remote name format!!!")]
[CustomValidation(typeof(AddRemote), nameof(ValidateRemoteName))]
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
[Required(ErrorMessage = "Remote URL is required!!!")]
[CustomValidation(typeof(AddRemote), nameof(ValidateRemoteURL))]
public string Url {
public string Url
{
get => _url;
set {
set
{
if (SetProperty(ref _url, value, true)) UseSSH = Models.Remote.IsSSH(value);
}
}
public bool UseSSH {
public bool UseSSH
{
get => _useSSH;
set => SetProperty(ref _useSSH, value);
}
public string SSHKey {
public string SSHKey
{
get;
set;
}
public AddRemote(Repository repo) {
public AddRemote(Repository repo)
{
_repo = repo;
View = new Views.AddRemote() { DataContext = this };
}
public static ValidationResult ValidateRemoteName(string name, ValidationContext ctx) {
if (ctx.ObjectInstance is AddRemote add) {
public static ValidationResult ValidateRemoteName(string name, ValidationContext ctx)
{
if (ctx.ObjectInstance is AddRemote add)
{
var exists = add._repo.Remotes.Find(x => x.Name == name);
if (exists != null) return new ValidationResult("A remote with given name already exists!!!");
}
@ -44,8 +54,10 @@ namespace SourceGit.ViewModels {
return ValidationResult.Success;
}
public static ValidationResult ValidateRemoteURL(string url, ValidationContext ctx) {
if (ctx.ObjectInstance is AddRemote add) {
public static ValidationResult ValidateRemoteURL(string url, ValidationContext ctx)
{
if (ctx.ObjectInstance is AddRemote add)
{
if (!Models.Remote.IsValidURL(url)) return new ValidationResult("Bad remote URL format!!!");
var exists = add._repo.Remotes.Find(x => x.URL == url);
@ -55,22 +67,27 @@ namespace SourceGit.ViewModels {
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Adding remote ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Remote(_repo.FullPath).Add(_name, _url);
if (succ) {
if (succ)
{
SetProgressDescription("Fetching from added remote ...");
new Commands.Fetch(_repo.FullPath, _name, true, SetProgressDescription).Exec();
if (_useSSH) {
if (_useSSH)
{
SetProgressDescription("Post processing ...");
new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", SSHKey);
}
}
CallUIThread(() => {
CallUIThread(() =>
{
_repo.MarkBranchesDirtyManually();
_repo.SetWatcherEnabled(true);
});
@ -78,9 +95,9 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _name = string.Empty;
private string _url = string.Empty;
private bool _useSSH = false;
}
}
}

View file

@ -2,62 +2,74 @@
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class AddSubmodule : Popup {
namespace SourceGit.ViewModels
{
public class AddSubmodule : Popup
{
[Required(ErrorMessage = "Url is required!!!")]
[CustomValidation(typeof(AddSubmodule), nameof(ValidateURL))]
public string Url {
public string Url
{
get => _url;
set => SetProperty(ref _url, value, true);
}
[Required(ErrorMessage = "Reletive path is required!!!")]
[CustomValidation(typeof(AddSubmodule), nameof(ValidateRelativePath))]
public string RelativePath {
public string RelativePath
{
get => _relativePath;
set => SetProperty(ref _relativePath, value, true);
}
public bool Recursive {
public bool Recursive
{
get;
set;
}
public AddSubmodule(Repository repo) {
public AddSubmodule(Repository repo)
{
_repo = repo;
View = new Views.AddSubmodule() { DataContext = this };
}
public static ValidationResult ValidateURL(string url, ValidationContext ctx) {
public static ValidationResult ValidateURL(string url, ValidationContext ctx)
{
if (!Models.Remote.IsValidURL(url)) return new ValidationResult("Invalid repository URL format");
return ValidationResult.Success;
}
public static ValidationResult ValidateRelativePath(string path, ValidationContext ctx) {
if (Path.Exists(path)) {
public static ValidationResult ValidateRelativePath(string path, ValidationContext ctx)
{
if (Path.Exists(path))
{
return new ValidationResult("Give path is exists already!");
}
if (Path.IsPathRooted(path)) {
if (Path.IsPathRooted(path))
{
return new ValidationResult("Path must be relative to this repository!");
}
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Adding submodule...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Submodule(_repo.FullPath).Add(_url, _relativePath, Recursive, SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _url = string.Empty;
private string _relativePath = string.Empty;
}
}
}

View file

@ -3,31 +3,38 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Apply : Popup {
namespace SourceGit.ViewModels
{
public class Apply : Popup
{
[Required(ErrorMessage = "Patch file is required!!!")]
[CustomValidation(typeof(Apply), nameof(ValidatePatchFile))]
public string PatchFile {
public string PatchFile
{
get => _patchFile;
set => SetProperty(ref _patchFile, value, true);
}
public bool IgnoreWhiteSpace {
public bool IgnoreWhiteSpace
{
get => _ignoreWhiteSpace;
set => SetProperty(ref _ignoreWhiteSpace, value);
}
public List<Models.ApplyWhiteSpaceMode> WhiteSpaceModes {
public List<Models.ApplyWhiteSpaceMode> WhiteSpaceModes
{
get;
private set;
}
public Models.ApplyWhiteSpaceMode SelectedWhiteSpaceMode {
public Models.ApplyWhiteSpaceMode SelectedWhiteSpaceMode
{
get;
set;
}
public Apply(Repository repo) {
public Apply(Repository repo)
{
_repo = repo;
WhiteSpaceModes = new List<Models.ApplyWhiteSpaceMode> {
@ -41,27 +48,31 @@ namespace SourceGit.ViewModels {
View = new Views.Apply() { DataContext = this };
}
public static ValidationResult ValidatePatchFile(string file, ValidationContext _) {
if (File.Exists(file)) {
public static ValidationResult ValidatePatchFile(string file, ValidationContext _)
{
if (File.Exists(file))
{
return ValidationResult.Success;
}
return new ValidationResult($"File '{file}' can NOT be found!!!");
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Apply patch...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, null).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _patchFile = string.Empty;
private bool _ignoreWhiteSpace = true;
}
}
}

View file

@ -2,21 +2,26 @@
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Archive : Popup {
namespace SourceGit.ViewModels
{
public class Archive : Popup
{
[Required(ErrorMessage = "Output file name is required")]
public string SaveFile {
public string SaveFile
{
get => _saveFile;
set => SetProperty(ref _saveFile, value, true);
}
public object BasedOn {
public object BasedOn
{
get;
private set;
}
public Archive(Repository repo, Models.Branch branch) {
public Archive(Repository repo, Models.Branch branch)
{
_repo = repo;
_revision = branch.Head;
_saveFile = $"archive-{Path.GetFileNameWithoutExtension(branch.Name)}.zip";
@ -24,15 +29,17 @@ namespace SourceGit.ViewModels {
View = new Views.Archive() { DataContext = this };
}
public Archive(Repository repo, Models.Commit commit) {
public Archive(Repository repo, Models.Commit commit)
{
_repo = repo;
_revision = commit.SHA;
_saveFile = $"archive-{commit.SHA.Substring(0,10)}.zip";
_saveFile = $"archive-{commit.SHA.Substring(0, 10)}.zip";
BasedOn = commit;
View = new Views.Archive() { DataContext = this };
}
public Archive(Repository repo, Models.Tag tag) {
public Archive(Repository repo, Models.Tag tag)
{
_repo = repo;
_revision = tag.SHA;
_saveFile = $"archive-{tag.Name}.zip";
@ -40,13 +47,16 @@ namespace SourceGit.ViewModels {
View = new Views.Archive() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Archiving ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Archive(_repo.FullPath, _revision, _saveFile, SetProgressDescription).Exec();
CallUIThread(() => {
CallUIThread(() =>
{
_repo.SetWatcherEnabled(true);
if (succ) App.SendNotification(_repo.FullPath, $"Save archive to : {_saveFile}");
});
@ -55,8 +65,8 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _saveFile = string.Empty;
private string _revision = string.Empty;
private readonly string _revision = string.Empty;
}
}
}

View file

@ -1,30 +1,38 @@
using Avalonia.Collections;
using Avalonia.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class AssumeUnchangedManager {
using Avalonia.Collections;
using Avalonia.Threading;
namespace SourceGit.ViewModels
{
public class AssumeUnchangedManager
{
public AvaloniaList<string> Files { get; private set; }
public AssumeUnchangedManager(string repo) {
public AssumeUnchangedManager(string repo)
{
_repo = repo;
Files = new AvaloniaList<string>();
Task.Run(() => {
Task.Run(() =>
{
var collect = new Commands.AssumeUnchanged(_repo).View();
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
Files.AddRange(collect);
});
});
}
public void Remove(object param) {
if (param is string file) {
public void Remove(object param)
{
if (param is string file)
{
new Commands.AssumeUnchanged(_repo).Remove(file);
Files.Remove(file);
}
}
private string _repo;
private readonly string _repo;
}
}
}

View file

@ -1,48 +1,60 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Blame : ObservableObject {
public string Title {
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Blame : ObservableObject
{
public string Title
{
get;
private set;
}
public string SelectedSHA {
public string SelectedSHA
{
get => _selectedSHA;
private set => SetProperty(ref _selectedSHA, value);
}
public bool IsBinary {
public bool IsBinary
{
get => _data != null && _data.IsBinary;
}
public Models.BlameData Data {
public Models.BlameData Data
{
get => _data;
private set => SetProperty(ref _data, value);
}
public Blame(string repo, string file, string revision) {
public Blame(string repo, string file, string revision)
{
_repo = repo;
Title = $"{file} @ {revision.Substring(0, 10)}";
Task.Run(() => {
Task.Run(() =>
{
var result = new Commands.Blame(repo, file, revision).Result();
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
Data = result;
OnPropertyChanged(nameof(IsBinary));
});
});
}
public void NavigateToCommit(string commitSHA) {
public void NavigateToCommit(string commitSHA)
{
var repo = Preference.FindRepository(_repo);
if (repo != null) repo.NavigateToCommit(commitSHA);
}
private string _repo = string.Empty;
private readonly string _repo = string.Empty;
private string _selectedSHA = string.Empty;
private Models.BlameData _data = null;
}
}
}

View file

@ -1,29 +1,35 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Checkout : Popup {
public string Branch {
namespace SourceGit.ViewModels
{
public class Checkout : Popup
{
public string Branch
{
get;
private set;
}
public Checkout(Repository repo, string branch) {
public Checkout(Repository repo, string branch)
{
_repo = repo;
Branch = branch;
View = new Views.Checkout() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Checkout '{Branch}' ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Checkout(_repo.FullPath).Branch(Branch, SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo;
private readonly Repository _repo;
}
}
}

View file

@ -1,35 +1,42 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class CherryPick : Popup {
public Models.Commit Target {
namespace SourceGit.ViewModels
{
public class CherryPick : Popup
{
public Models.Commit Target
{
get;
private set;
}
public bool AutoCommit {
public bool AutoCommit
{
get;
set;
}
public CherryPick(Repository repo, Models.Commit target) {
public CherryPick(Repository repo, Models.Commit target)
{
_repo = repo;
Target = target;
AutoCommit = true;
View = new Views.CherryPick() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Cherry-Pick commit '{Target.SHA}' ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.CherryPick(_repo.FullPath, Target.SHA, !AutoCommit).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,21 +1,27 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Cleanup : Popup {
public Cleanup(Repository repo) {
namespace SourceGit.ViewModels
{
public class Cleanup : Popup
{
public Cleanup(Repository repo)
{
_repo = repo;
View = new Views.Cleanup() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Cleanup (GC & prune) ...";
return Task.Run(() => {
return Task.Run(() =>
{
new Commands.GC(_repo.FullPath, SetProgressDescription).Exec();
var lfs = new Commands.LFS(_repo.FullPath);
if (lfs.IsEnabled()) {
if (lfs.IsEnabled())
{
SetProgressDescription("Run LFS prune ...");
lfs.Prune(SetProgressDescription);
}
@ -25,6 +31,6 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,23 +1,28 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class ClearStashes : Popup {
public ClearStashes(Repository repo) {
namespace SourceGit.ViewModels
{
public class ClearStashes : Popup
{
public ClearStashes(Repository repo)
{
_repo = repo;
View = new Views.ClearStashes() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Clear all stashes...";
return Task.Run(() => {
return Task.Run(() =>
{
new Commands.Stash(_repo.FullPath).Clear();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -3,92 +3,114 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Clone : Popup {
namespace SourceGit.ViewModels
{
public class Clone : Popup
{
[Required(ErrorMessage = "Remote URL is required")]
[CustomValidation(typeof(Clone), nameof(ValidateRemote))]
public string Remote {
public string Remote
{
get => _remote;
set {
set
{
if (SetProperty(ref _remote, value, true)) UseSSH = Models.Remote.IsSSH(value);
}
}
public bool UseSSH {
public bool UseSSH
{
get => _useSSH;
set => SetProperty(ref _useSSH, value);
}
public string SSHKey {
public string SSHKey
{
get => _sshKey;
set => SetProperty(ref _sshKey, value);
}
[Required(ErrorMessage = "Parent folder is required")]
[CustomValidation(typeof(Clone), nameof(ValidateParentFolder))]
public string ParentFolder {
public string ParentFolder
{
get => _parentFolder;
set => SetProperty(ref _parentFolder, value, true);
}
public string Local {
public string Local
{
get => _local;
set => SetProperty(ref _local, value);
}
public string ExtraArgs {
public string ExtraArgs
{
get => _extraArgs;
set => SetProperty(ref _extraArgs, value);
}
public Clone(Launcher launcher, LauncherPage page) {
public Clone(Launcher launcher, LauncherPage page)
{
_launcher = launcher;
_page = page;
View = new Views.Clone() { DataContext = this };
}
public static ValidationResult ValidateRemote(string remote, ValidationContext _) {
public static ValidationResult ValidateRemote(string remote, ValidationContext _)
{
if (!Models.Remote.IsValidURL(remote)) return new ValidationResult("Invalid remote repository URL format");
return ValidationResult.Success;
}
public static ValidationResult ValidateParentFolder(string folder, ValidationContext _) {
public static ValidationResult ValidateParentFolder(string folder, ValidationContext _)
{
if (!Directory.Exists(folder)) return new ValidationResult("Given path can NOT be found");
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
ProgressDescription = "Clone ...";
return Task.Run(() => {
return Task.Run(() =>
{
var cmd = new Commands.Clone(HostPageId, _parentFolder, _remote, _local, _useSSH ? _sshKey : "", _extraArgs, SetProgressDescription);
if (!cmd.Exec()) return false;
var path = _parentFolder;
if (!string.IsNullOrEmpty(_local)) {
if (!string.IsNullOrEmpty(_local))
{
path = Path.GetFullPath(Path.Combine(path, _local));
} else {
}
else
{
var name = Path.GetFileName(_remote);
if (name.EndsWith(".git")) name = name.Substring(0, name.Length - 4);
path = Path.GetFullPath(Path.Combine(path, name));
}
if (!Directory.Exists(path)) {
CallUIThread(() => {
if (!Directory.Exists(path))
{
CallUIThread(() =>
{
App.RaiseException(HostPageId, $"Folder '{path}' can NOT be found");
});
return false;
}
if (_useSSH && !string.IsNullOrEmpty(_sshKey)) {
if (_useSSH && !string.IsNullOrEmpty(_sshKey))
{
var config = new Commands.Config(path);
config.Set("remote.origin.sshkey", _sshKey);
}
CallUIThread(() => {
CallUIThread(() =>
{
var repo = Preference.AddRepository(path, Path.Combine(path, ".git"));
var node = new RepositoryNode() {
var node = new RepositoryNode()
{
Id = repo.FullPath,
Name = Path.GetFileName(repo.FullPath),
Bookmark = 0,
@ -103,8 +125,8 @@ namespace SourceGit.ViewModels {
});
}
private Launcher _launcher = null;
private LauncherPage _page = null;
private readonly Launcher _launcher = null;
private readonly LauncherPage _page = null;
private string _remote = string.Empty;
private bool _useSSH = false;
private string _sshKey = string.Empty;
@ -112,4 +134,4 @@ namespace SourceGit.ViewModels {
private string _local = string.Empty;
private string _extraArgs = string.Empty;
}
}
}

View file

@ -1,54 +1,71 @@
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class CommitDetail : ObservableObject {
public DiffContext DiffContext {
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class CommitDetail : ObservableObject
{
public DiffContext DiffContext
{
get => _diffContext;
private set => SetProperty(ref _diffContext, value);
}
public int ActivePageIndex {
public int ActivePageIndex
{
get => _activePageIndex;
set => SetProperty(ref _activePageIndex, value);
}
public Models.Commit Commit {
public Models.Commit Commit
{
get => _commit;
set {
set
{
if (SetProperty(ref _commit, value)) Refresh();
}
}
public List<Models.Change> Changes {
public List<Models.Change> Changes
{
get => _changes;
set => SetProperty(ref _changes, value);
}
public List<Models.Change> VisibleChanges {
public List<Models.Change> VisibleChanges
{
get => _visibleChanges;
set => SetProperty(ref _visibleChanges, value);
}
public List<FileTreeNode> ChangeTree {
public List<FileTreeNode> ChangeTree
{
get => _changeTree;
set => SetProperty(ref _changeTree, value);
}
public Models.Change SelectedChange {
public Models.Change SelectedChange
{
get => _selectedChange;
set {
if (SetProperty(ref _selectedChange, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedChange, value))
{
if (value == null)
{
SelectedChangeNode = null;
DiffContext = null;
} else {
}
else
{
SelectedChangeNode = FileTreeNode.SelectByPath(_changeTree, value.Path);
DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value));
}
@ -56,63 +73,84 @@ namespace SourceGit.ViewModels {
}
}
public FileTreeNode SelectedChangeNode {
public FileTreeNode SelectedChangeNode
{
get => _selectedChangeNode;
set {
if (SetProperty(ref _selectedChangeNode, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedChangeNode, value))
{
if (value == null)
{
SelectedChange = null;
} else {
}
else
{
SelectedChange = value.Backend as Models.Change;
}
}
}
}
public string SearchChangeFilter {
public string SearchChangeFilter
{
get => _searchChangeFilter;
set {
if (SetProperty(ref _searchChangeFilter, value)) {
set
{
if (SetProperty(ref _searchChangeFilter, value))
{
RefreshVisibleChanges();
}
}
}
public List<FileTreeNode> RevisionFilesTree {
public List<FileTreeNode> RevisionFilesTree
{
get => _revisionFilesTree;
set => SetProperty(ref _revisionFilesTree, value);
}
public FileTreeNode SelectedRevisionFileNode {
public FileTreeNode SelectedRevisionFileNode
{
get => _selectedRevisionFileNode;
set {
if (SetProperty(ref _selectedRevisionFileNode, value) && value != null && !value.IsFolder) {
set
{
if (SetProperty(ref _selectedRevisionFileNode, value) && value != null && !value.IsFolder)
{
RefreshViewRevisionFile(value.Backend as Models.Object);
} else {
}
else
{
ViewRevisionFileContent = null;
}
}
}
public string SearchFileFilter {
public string SearchFileFilter
{
get => _searchFileFilter;
set {
if (SetProperty(ref _searchFileFilter, value)) {
set
{
if (SetProperty(ref _searchFileFilter, value))
{
RefreshVisibleFiles();
}
}
}
public object ViewRevisionFileContent {
public object ViewRevisionFileContent
{
get => _viewRevisionFileContent;
set => SetProperty(ref _viewRevisionFileContent, value);
}
public CommitDetail(string repo) {
public CommitDetail(string repo)
{
_repo = repo;
}
public void Cleanup() {
public void Cleanup()
{
_repo = null;
_commit = null;
if (_changes != null) _changes.Clear();
@ -130,27 +168,33 @@ namespace SourceGit.ViewModels {
_cancelToken = null;
}
public void NavigateTo(string commitSHA) {
public void NavigateTo(string commitSHA)
{
var repo = Preference.FindRepository(_repo);
if (repo != null) repo.NavigateToCommit(commitSHA);
}
public void ClearSearchChangeFilter() {
public void ClearSearchChangeFilter()
{
SearchChangeFilter = string.Empty;
}
public void ClearSearchFileFilter() {
public void ClearSearchFileFilter()
{
SearchFileFilter = string.Empty;
}
public ContextMenu CreateChangeContextMenu(Models.Change change) {
public ContextMenu CreateChangeContextMenu(Models.Change change)
{
var menu = new ContextMenu();
if (change.Index != Models.ChangeState.Deleted) {
if (change.Index != Models.ChangeState.Deleted)
{
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) => {
history.Click += (_, ev) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
window.Show();
ev.Handled = true;
@ -159,7 +203,8 @@ namespace SourceGit.ViewModels {
var blame = new MenuItem();
blame.Header = App.Text("Blame");
blame.Icon = App.CreateMenuIcon("Icons.Blame");
blame.Click += (o, ev) => {
blame.Click += (o, ev) =>
{
var window = new Views.Blame() { DataContext = new Blame(_repo, change.Path, _commit.SHA) };
window.Show();
ev.Handled = true;
@ -170,7 +215,8 @@ namespace SourceGit.ViewModels {
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Folder.Open");
explore.IsEnabled = File.Exists(full);
explore.Click += (_, ev) => {
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(full, true);
ev.Handled = true;
};
@ -183,7 +229,8 @@ namespace SourceGit.ViewModels {
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Click += (_, ev) => {
copyPath.Click += (_, ev) =>
{
App.CopyText(change.Path);
ev.Handled = true;
};
@ -192,11 +239,13 @@ namespace SourceGit.ViewModels {
return menu;
}
public ContextMenu CreateRevisionFileContextMenu(Models.Object file) {
public ContextMenu CreateRevisionFileContextMenu(Models.Object file)
{
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, ev) => {
history.Click += (_, ev) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path) };
window.Show();
ev.Handled = true;
@ -205,7 +254,8 @@ namespace SourceGit.ViewModels {
var blame = new MenuItem();
blame.Header = App.Text("Blame");
blame.Icon = App.CreateMenuIcon("Icons.Blame");
blame.Click += (o, ev) => {
blame.Click += (o, ev) =>
{
var window = new Views.Blame() { DataContext = new Blame(_repo, file.Path, _commit.SHA) };
window.Show();
ev.Handled = true;
@ -215,7 +265,8 @@ namespace SourceGit.ViewModels {
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Folder.Open");
explore.Click += (_, ev) => {
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(full, file.Type == Models.ObjectType.Blob);
ev.Handled = true;
};
@ -224,13 +275,15 @@ namespace SourceGit.ViewModels {
saveAs.Header = App.Text("SaveAs");
saveAs.Icon = App.CreateMenuIcon("Icons.Save");
saveAs.IsEnabled = file.Type == Models.ObjectType.Blob;
saveAs.Click += async (_, ev) => {
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) {
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);
}
@ -241,7 +294,8 @@ namespace SourceGit.ViewModels {
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Click += (_, ev) => {
copyPath.Click += (_, ev) =>
{
App.CopyText(file.Path);
ev.Handled = true;
};
@ -255,7 +309,8 @@ namespace SourceGit.ViewModels {
return menu;
}
private void Refresh() {
private void Refresh()
{
_changes = null;
VisibleChanges = null;
SelectedChange = null;
@ -268,59 +323,75 @@ namespace SourceGit.ViewModels {
var cmdChanges = new Commands.QueryCommitChanges(_repo, _commit.SHA) { Cancel = _cancelToken };
var cmdRevisionFiles = new Commands.QueryRevisionObjects(_repo, _commit.SHA) { Cancel = _cancelToken };
Task.Run(() => {
Task.Run(() =>
{
var changes = cmdChanges.Result();
if (cmdChanges.Cancel.Requested) return;
var visible = changes;
if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) {
if (!string.IsNullOrWhiteSpace(_searchChangeFilter))
{
visible = new List<Models.Change>();
foreach (var c in changes) {
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) {
foreach (var c in changes)
{
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(c);
}
}
}
var tree = FileTreeNode.Build(visible);
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
Changes = changes;
VisibleChanges = visible;
ChangeTree = tree;
});
});
Task.Run(() => {
Task.Run(() =>
{
var files = cmdRevisionFiles.Result();
if (cmdRevisionFiles.Cancel.Requested) return;
var visible = files;
if (!string.IsNullOrWhiteSpace(_searchFileFilter)) {
if (!string.IsNullOrWhiteSpace(_searchFileFilter))
{
visible = new List<Models.Object>();
foreach (var f in files) {
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) {
foreach (var f in files)
{
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(f);
}
}
}
var tree = FileTreeNode.Build(visible);
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
_revisionFiles = files;
RevisionFilesTree = tree;
});
});
}
private void RefreshVisibleChanges() {
private void RefreshVisibleChanges()
{
if (_changes == null) return;
if (string.IsNullOrEmpty(_searchChangeFilter)) {
if (string.IsNullOrEmpty(_searchChangeFilter))
{
VisibleChanges = _changes;
} else {
}
else
{
var visible = new List<Models.Change>();
foreach (var c in _changes) {
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) {
foreach (var c in _changes)
{
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(c);
}
}
@ -331,14 +402,18 @@ namespace SourceGit.ViewModels {
ChangeTree = FileTreeNode.Build(_visibleChanges);
}
private void RefreshVisibleFiles() {
private void RefreshVisibleFiles()
{
if (_revisionFiles == null) return;
var visible = _revisionFiles;
if (!string.IsNullOrWhiteSpace(_searchFileFilter)) {
if (!string.IsNullOrWhiteSpace(_searchFileFilter))
{
visible = new List<Models.Object>();
foreach (var f in _revisionFiles) {
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) {
foreach (var f in _revisionFiles)
{
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(f);
}
}
@ -347,52 +422,66 @@ namespace SourceGit.ViewModels {
RevisionFilesTree = FileTreeNode.Build(visible);
}
private void RefreshViewRevisionFile(Models.Object file) {
switch (file.Type) {
case Models.ObjectType.Blob:
Task.Run(() => {
var isBinary = new Commands.IsBinary(_repo, _commit.SHA, file.Path).Result();
if (isBinary) {
var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result();
Dispatcher.UIThread.Invoke(() => {
ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size };
});
return;
}
var content = new Commands.QueryFileContent(_repo, _commit.SHA, file.Path).Result();
if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.Ordinal)) {
var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (lines.Length == 3) {
foreach (var line in lines) {
if (line.StartsWith("oid sha256:", StringComparison.Ordinal)) {
obj.Object.Oid = line.Substring(11);
} else if (line.StartsWith("size ", StringComparison.Ordinal)) {
obj.Object.Size = long.Parse(line.Substring(5));
}
}
Dispatcher.UIThread.Invoke(() => {
ViewRevisionFileContent = obj;
private void RefreshViewRevisionFile(Models.Object file)
{
switch (file.Type)
{
case Models.ObjectType.Blob:
Task.Run(() =>
{
var isBinary = new Commands.IsBinary(_repo, _commit.SHA, file.Path).Result();
if (isBinary)
{
var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result();
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size };
});
return;
}
}
}
Dispatcher.UIThread.Invoke(() => {
ViewRevisionFileContent = new Models.RevisionTextFile() {
FileName = file.Path,
Content = content
};
var content = new Commands.QueryFileContent(_repo, _commit.SHA, file.Path).Result();
if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.Ordinal))
{
var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (lines.Length == 3)
{
foreach (var line in lines)
{
if (line.StartsWith("oid sha256:", StringComparison.Ordinal))
{
obj.Object.Oid = line.Substring(11);
}
else if (line.StartsWith("size ", StringComparison.Ordinal))
{
obj.Object.Size = long.Parse(line.Substring(5));
}
}
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = obj;
});
return;
}
}
Dispatcher.UIThread.Invoke(() =>
{
ViewRevisionFileContent = new Models.RevisionTextFile()
{
FileName = file.Path,
Content = content
};
});
});
});
break;
case Models.ObjectType.Commit:
ViewRevisionFileContent = new Models.RevisionSubmodule() { SHA = file.SHA };
break;
default:
ViewRevisionFileContent = null;
break;
break;
case Models.ObjectType.Commit:
ViewRevisionFileContent = new Models.RevisionSubmodule() { SHA = file.SHA };
break;
default:
ViewRevisionFileContent = null;
break;
}
}
@ -413,4 +502,4 @@ namespace SourceGit.ViewModels {
private object _viewRevisionFileContent = null;
private Commands.Command.CancelToken _cancelToken = null;
}
}
}

View file

@ -1,36 +1,44 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class CreateBranch : Popup {
namespace SourceGit.ViewModels
{
public class CreateBranch : Popup
{
[Required(ErrorMessage = "Branch name is required!")]
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
[CustomValidation(typeof(CreateBranch), nameof(ValidateBranchName))]
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
public object BasedOn {
public object BasedOn
{
get;
private set;
}
public bool CheckoutAfterCreated {
public bool CheckoutAfterCreated
{
get;
set;
} = true;
public bool AutoStash {
public bool AutoStash
{
get;
set;
} = true;
public CreateBranch(Repository repo, Models.Branch branch) {
public CreateBranch(Repository repo, Models.Branch branch)
{
_repo = repo;
_baseOnRevision = branch.FullName;
if (!branch.IsLocal && repo.Branches.Find(x => x.IsLocal && x.Name == branch.Name) == null) {
if (!branch.IsLocal && repo.Branches.Find(x => x.IsLocal && x.Name == branch.Name) == null)
{
Name = branch.Name;
}
@ -38,7 +46,8 @@ namespace SourceGit.ViewModels {
View = new Views.CreateBranch() { DataContext = this };
}
public CreateBranch(Repository repo, Models.Commit commit) {
public CreateBranch(Repository repo, Models.Commit commit)
{
_repo = repo;
_baseOnRevision = commit.SHA;
@ -46,7 +55,8 @@ namespace SourceGit.ViewModels {
View = new Views.CreateBranch() { DataContext = this };
}
public CreateBranch(Repository repo, Models.Tag tag) {
public CreateBranch(Repository repo, Models.Tag tag)
{
_repo = repo;
_baseOnRevision = tag.SHA;
@ -54,11 +64,13 @@ namespace SourceGit.ViewModels {
View = new Views.CreateBranch() { DataContext = this };
}
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) {
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx)
{
var creator = ctx.ObjectInstance as CreateBranch;
if (creator == null) return new ValidationResult("Missing runtime context to create branch!");
foreach (var b in creator._repo.Branches) {
foreach (var b in creator._repo.Branches)
{
var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}";
if (test == name) return new ValidationResult("A branch with same name already exists!");
}
@ -66,27 +78,36 @@ namespace SourceGit.ViewModels {
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
return Task.Run(() => {
if (CheckoutAfterCreated) {
return Task.Run(() =>
{
if (CheckoutAfterCreated)
{
bool needPopStash = false;
if (_repo.WorkingCopyChangesCount > 0) {
if (AutoStash) {
if (_repo.WorkingCopyChangesCount > 0)
{
if (AutoStash)
{
SetProgressDescription("Adding untracked changes...");
var succ = new Commands.Add(_repo.FullPath).Exec();
if (succ) {
if (succ)
{
SetProgressDescription("Stash local changes");
succ = new Commands.Stash(_repo.FullPath).Push("CREATE_BRANCH_AUTO_STASH");
}
if (!succ) {
if (!succ)
{
CallUIThread(() => _repo.SetWatcherEnabled(true));
return false;
}
needPopStash = true;
} else {
}
else
{
SetProgressDescription("Discard local changes...");
Commands.Discard.All(_repo.FullPath);
}
@ -95,11 +116,14 @@ namespace SourceGit.ViewModels {
SetProgressDescription($"Create new branch '{_name}'");
new Commands.Checkout(_repo.FullPath).Branch(_name, _baseOnRevision, SetProgressDescription);
if (needPopStash) {
if (needPopStash)
{
SetProgressDescription("Re-apply local changes...");
new Commands.Stash(_repo.FullPath).Pop("stash@{0}");
}
} else {
}
else
{
Commands.Branch.Create(_repo.FullPath, _name, _baseOnRevision);
}
@ -108,8 +132,8 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _name = null;
private string _baseOnRevision = null;
private readonly string _baseOnRevision = null;
}
}
}

View file

@ -2,21 +2,27 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class CreateGroup : Popup {
namespace SourceGit.ViewModels
{
public class CreateGroup : Popup
{
[Required(ErrorMessage = "Group name is required!")]
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
public CreateGroup(RepositoryNode parent) {
public CreateGroup(RepositoryNode parent)
{
_parent = parent;
View = new Views.CreateGroup() { DataContext = this };
}
public override Task<bool> Sure() {
Preference.AddNode(new RepositoryNode() {
public override Task<bool> Sure()
{
Preference.AddNode(new RepositoryNode()
{
Id = Guid.NewGuid().ToString(),
Name = _name,
IsRepository = false,
@ -26,7 +32,7 @@ namespace SourceGit.ViewModels {
return null;
}
private RepositoryNode _parent = null;
private readonly RepositoryNode _parent = null;
private string _name = string.Empty;
}
}
}

View file

@ -3,27 +3,33 @@ using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class CreateTag : Popup {
namespace SourceGit.ViewModels
{
public class CreateTag : Popup
{
[Required(ErrorMessage = "Tag name is required!")]
[RegularExpression(@"^[\w\-\.]+$", ErrorMessage = "Bad tag name format!")]
[CustomValidation(typeof(CreateTag), nameof(ValidateTagName))]
public string TagName {
public string TagName
{
get => _tagName;
set => SetProperty(ref _tagName, value, true);
}
public string Message {
public string Message
{
get;
set;
}
public object BasedOn {
public object BasedOn
{
get;
private set;
}
public CreateTag(Repository repo, Models.Branch branch) {
public CreateTag(Repository repo, Models.Branch branch)
{
_repo = repo;
_basedOn = branch.Head;
@ -31,7 +37,8 @@ namespace SourceGit.ViewModels {
View = new Views.CreateTag() { DataContext = this };
}
public CreateTag(Repository repo, Models.Commit commit) {
public CreateTag(Repository repo, Models.Commit commit)
{
_repo = repo;
_basedOn = commit.SHA;
@ -39,28 +46,32 @@ namespace SourceGit.ViewModels {
View = new Views.CreateTag() { DataContext = this };
}
public static ValidationResult ValidateTagName(string name, ValidationContext ctx) {
public static ValidationResult ValidateTagName(string name, ValidationContext ctx)
{
var creator = ctx.ObjectInstance as CreateTag;
if (creator != null) {
if (creator != null)
{
var found = creator._repo.Tags.Find(x => x.Name == name);
if (found != null) return new ValidationResult("A tag with same name already exists!");
}
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Create tag...";
return Task.Run(() => {
return Task.Run(() =>
{
Commands.Tag.Add(_repo.FullPath, TagName, _basedOn, Message);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _tagName = string.Empty;
private string _basedOn = string.Empty;
private readonly string _basedOn = string.Empty;
}
}
}

View file

@ -1,26 +1,35 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class DeleteBranch : Popup {
public Models.Branch Target {
namespace SourceGit.ViewModels
{
public class DeleteBranch : Popup
{
public Models.Branch Target
{
get;
private set;
}
public DeleteBranch(Repository repo, Models.Branch branch) {
public DeleteBranch(Repository repo, Models.Branch branch)
{
_repo = repo;
Target = branch;
View = new Views.DeleteBranch() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Deleting branch...";
return Task.Run(() => {
if (Target.IsLocal) {
return Task.Run(() =>
{
if (Target.IsLocal)
{
Commands.Branch.Delete(_repo.FullPath, Target.Name);
} else {
}
else
{
new Commands.Push(_repo.FullPath, Target.Remote, Target.Name).Exec();
}
@ -29,6 +38,6 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,25 +1,32 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class DeleteRemote : Popup {
public Models.Remote Remote {
namespace SourceGit.ViewModels
{
public class DeleteRemote : Popup
{
public Models.Remote Remote
{
get;
private set;
}
public DeleteRemote(Repository repo, Models.Remote remote) {
public DeleteRemote(Repository repo, Models.Remote remote)
{
_repo = repo;
Remote = remote;
View = new Views.DeleteRemote() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Deleting remote ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Remote(_repo.FullPath).Delete(Remote.Name);
CallUIThread(() => {
CallUIThread(() =>
{
_repo.MarkBranchesDirtyManually();
_repo.SetWatcherEnabled(true);
});
@ -27,6 +34,6 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,22 +1,27 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class DeleteRepositoryNode : Popup {
public RepositoryNode Node {
namespace SourceGit.ViewModels
{
public class DeleteRepositoryNode : Popup
{
public RepositoryNode Node
{
get => _node;
set => SetProperty(ref _node, value);
}
public DeleteRepositoryNode(RepositoryNode node) {
public DeleteRepositoryNode(RepositoryNode node)
{
_node = node;
View = new Views.DeleteRepositoryNode() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
Preference.RemoveNode(_node);
return null;
}
private RepositoryNode _node = null;
}
}
}

View file

@ -1,30 +1,36 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class DeleteSubmodule : Popup {
namespace SourceGit.ViewModels
{
public class DeleteSubmodule : Popup
{
public string Submodule {
public string Submodule
{
get;
private set;
}
public DeleteSubmodule(Repository repo, string submodule) {
public DeleteSubmodule(Repository repo, string submodule)
{
_repo = repo;
Submodule = submodule;
View = new Views.DeleteSubmodule() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Deleting submodule ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Submodule(_repo.FullPath).Delete(Submodule);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,29 +1,36 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class DeleteTag : Popup {
public Models.Tag Target {
namespace SourceGit.ViewModels
{
public class DeleteTag : Popup
{
public Models.Tag Target
{
get;
private set;
}
public bool ShouldPushToRemote {
public bool ShouldPushToRemote
{
get;
set;
}
public DeleteTag(Repository repo, Models.Tag tag) {
public DeleteTag(Repository repo, Models.Tag tag)
{
_repo = repo;
Target = tag;
ShouldPushToRemote = true;
View = new Views.DeleteTag() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Deleting tag '{Target.Name}' ...";
return Task.Run(() => {
return Task.Run(() =>
{
var remotes = ShouldPushToRemote ? _repo.Remotes : null;
var succ = Commands.Tag.Delete(_repo.FullPath, Target.Name, remotes);
CallUIThread(() => _repo.SetWatcherEnabled(true));
@ -31,6 +38,6 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,61 +1,77 @@
using Avalonia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System.IO;
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class DiffContext : ObservableObject {
public string RepositoryPath {
using Avalonia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class DiffContext : ObservableObject
{
public string RepositoryPath
{
get => _repo;
}
public Models.Change WorkingCopyChange {
public Models.Change WorkingCopyChange
{
get => _option.WorkingCopyChange;
}
public bool IsUnstaged {
public bool IsUnstaged
{
get => _option.IsUnstaged;
}
public string FilePath {
public string FilePath
{
get => _option.Path;
}
public bool IsOrgFilePathVisible {
public bool IsOrgFilePathVisible
{
get => !string.IsNullOrWhiteSpace(_option.OrgPath) && _option.OrgPath != "/dev/null";
}
public string OrgFilePath {
public string OrgFilePath
{
get => _option.OrgPath;
}
public bool IsLoading {
public bool IsLoading
{
get => _isLoading;
private set => SetProperty(ref _isLoading, value);
}
public bool IsNoChange {
public bool IsNoChange
{
get => _isNoChange;
private set => SetProperty(ref _isNoChange, value);
}
public bool IsTextDiff {
public bool IsTextDiff
{
get => _isTextDiff;
private set => SetProperty(ref _isTextDiff, value);
}
public object Content {
public object Content
{
get => _content;
private set => SetProperty(ref _content, value);
}
public Vector SyncScrollOffset {
public Vector SyncScrollOffset
{
get => _syncScrollOffset;
set => SetProperty(ref _syncScrollOffset, value);
}
public DiffContext(string repo, Models.DiffOption option) {
public DiffContext(string repo, Models.DiffOption option)
{
_repo = repo;
_option = option;
@ -63,33 +79,46 @@ namespace SourceGit.ViewModels {
OnPropertyChanged(nameof(IsOrgFilePathVisible));
OnPropertyChanged(nameof(OrgFilePath));
Task.Run(() => {
Task.Run(() =>
{
var latest = new Commands.Diff(repo, option).Result();
var binaryDiff = null as Models.BinaryDiff;
if (latest.IsBinary) {
if (latest.IsBinary)
{
binaryDiff = new Models.BinaryDiff();
var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath;
if (option.Revisions.Count == 2) {
if (option.Revisions.Count == 2)
{
binaryDiff.OldSize = new Commands.QueryFileSize(repo, oldPath, option.Revisions[0]).Result();
binaryDiff.NewSize = new Commands.QueryFileSize(repo, _option.Path, option.Revisions[1]).Result();
} else {
}
else
{
binaryDiff.OldSize = new Commands.QueryFileSize(repo, oldPath, "HEAD").Result();
binaryDiff.NewSize = new FileInfo(Path.Combine(repo, _option.Path)).Length;
}
}
Dispatcher.UIThread.InvokeAsync(() => {
if (latest.IsBinary) {
Dispatcher.UIThread.InvokeAsync(() =>
{
if (latest.IsBinary)
{
Content = binaryDiff;
} else if (latest.IsLFS) {
}
else if (latest.IsLFS)
{
Content = latest.LFSDiff;
} else if (latest.TextDiff != null) {
}
else if (latest.TextDiff != null)
{
latest.TextDiff.File = _option.Path;
Content = latest.TextDiff;
IsTextDiff = true;
} else {
}
else
{
IsTextDiff = false;
IsNoChange = true;
}
@ -99,12 +128,14 @@ namespace SourceGit.ViewModels {
});
}
public async void OpenExternalMergeTool() {
public async void OpenExternalMergeTool()
{
var type = Preference.Instance.ExternalMergeToolType;
var exec = Preference.Instance.ExternalMergeToolPath;
var tool = Models.ExternalMergeTools.Supported.Find(x => x.Type == type);
if (tool == null || !File.Exists(exec)) {
if (tool == null || !File.Exists(exec))
{
App.RaiseException(_repo, "Invalid merge tool in preference setting!");
return;
}
@ -113,12 +144,12 @@ namespace SourceGit.ViewModels {
await Task.Run(() => Commands.MergeTool.OpenForDiff(_repo, exec, args, _option));
}
private string _repo = string.Empty;
private Models.DiffOption _option = null;
private readonly string _repo = string.Empty;
private readonly Models.DiffOption _option = null;
private bool _isLoading = true;
private bool _isNoChange = false;
private bool _isTextDiff = false;
private object _content = null;
private Vector _syncScrollOffset = Vector.Zero;
}
}
}

View file

@ -1,51 +1,68 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
namespace SourceGit.ViewModels
{
public class DiscardModeAll { }
public class DiscardModeSingle { public string File { get; set; } }
public class DiscardModeMulti { public int Count { get; set; } }
public class Discard : Popup {
public class Discard : Popup
{
public object Mode {
public object Mode
{
get;
private set;
}
public Discard(Repository repo) {
public Discard(Repository repo)
{
_repo = repo;
Mode = new DiscardModeAll();
View = new Views.Discard { DataContext = this };
}
public Discard(Repository repo, List<Models.Change> changes, bool isUnstaged) {
public Discard(Repository repo, List<Models.Change> changes, bool isUnstaged)
{
_repo = repo;
_changes = changes;
_isUnstaged = isUnstaged;
if (_changes == null) {
if (_changes == null)
{
Mode = new DiscardModeAll();
} else if (_changes.Count == 1) {
}
else if (_changes.Count == 1)
{
Mode = new DiscardModeSingle() { File = _changes[0].Path };
} else {
}
else
{
Mode = new DiscardModeMulti() { Count = _changes.Count };
}
View = new Views.Discard() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = _changes == null ? "Discard all local changes ..." : $"Discard total {_changes.Count} changes ...";
return Task.Run(() => {
if (_changes == null) {
return Task.Run(() =>
{
if (_changes == null)
{
Commands.Discard.All(_repo.FullPath);
} else if (_isUnstaged) {
}
else if (_isUnstaged)
{
Commands.Discard.ChangesInWorkTree(_repo.FullPath, _changes);
} else {
}
else
{
Commands.Discard.ChangesInStaged(_repo.FullPath, _changes);
}
@ -54,8 +71,8 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private List<Models.Change> _changes = null;
private bool _isUnstaged = true;
private readonly Repository _repo = null;
private readonly List<Models.Change> _changes = null;
private readonly bool _isUnstaged = true;
}
}
}

View file

@ -1,24 +1,29 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class DropStash : Popup {
namespace SourceGit.ViewModels
{
public class DropStash : Popup
{
public Models.Stash Stash { get; private set; }
public DropStash(string repo, Models.Stash stash) {
public DropStash(string repo, Models.Stash stash)
{
_repo = repo;
Stash = stash;
View = new Views.DropStash() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
ProgressDescription = $"Dropping stash: {Stash.Name}";
return Task.Run(() => {
return Task.Run(() =>
{
new Commands.Stash(_repo).Drop(Stash.Name);
return true;
});
}
private string _repo;
private readonly string _repo;
}
}
}

View file

@ -1,52 +1,64 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class EditRemote : Popup {
namespace SourceGit.ViewModels
{
public class EditRemote : Popup
{
[Required(ErrorMessage = "Remote name is required!!!")]
[RegularExpression(@"^[\w\-\.]+$", ErrorMessage = "Bad remote name format!!!")]
[CustomValidation(typeof(EditRemote), nameof(ValidateRemoteName))]
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
[Required(ErrorMessage = "Remote URL is required!!!")]
[CustomValidation(typeof(EditRemote), nameof(ValidateRemoteURL))]
public string Url {
public string Url
{
get => _url;
set {
set
{
if (SetProperty(ref _url, value, true)) UseSSH = Models.Remote.IsSSH(value);
}
}
public bool UseSSH {
public bool UseSSH
{
get => _useSSH;
set => SetProperty(ref _useSSH, value);
}
public string SSHKey {
public string SSHKey
{
get;
set;
}
public EditRemote(Repository repo, Models.Remote remote) {
public EditRemote(Repository repo, Models.Remote remote)
{
_repo = repo;
_remote = remote;
_name = remote.Name;
_url = remote.URL;
_useSSH = Models.Remote.IsSSH(remote.URL);
if (_useSSH) {
if (_useSSH)
{
SSHKey = new Commands.Config(repo.FullPath).Get($"remote.{remote.Name}.sshkey");
}
View = new Views.EditRemote() { DataContext = this };
}
public static ValidationResult ValidateRemoteName(string name, ValidationContext ctx) {
if (ctx.ObjectInstance is EditRemote edit) {
foreach (var remote in edit._repo.Remotes) {
public static ValidationResult ValidateRemoteName(string name, ValidationContext ctx)
{
if (ctx.ObjectInstance is EditRemote edit)
{
foreach (var remote in edit._repo.Remotes)
{
if (remote != edit._remote && name == remote.Name) new ValidationResult("A remote with given name already exists!!!");
}
}
@ -54,11 +66,14 @@ namespace SourceGit.ViewModels {
return ValidationResult.Success;
}
public static ValidationResult ValidateRemoteURL(string url, ValidationContext ctx) {
if (ctx.ObjectInstance is EditRemote edit) {
public static ValidationResult ValidateRemoteURL(string url, ValidationContext ctx)
{
if (ctx.ObjectInstance is EditRemote edit)
{
if (!Models.Remote.IsValidURL(url)) return new ValidationResult("Bad remote URL format!!!");
foreach (var remote in edit._repo.Remotes) {
foreach (var remote in edit._repo.Remotes)
{
if (remote != edit._remote && url == remote.URL) new ValidationResult("A remote with the same url already exists!!!");
}
}
@ -66,22 +81,27 @@ namespace SourceGit.ViewModels {
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Editing remote '{_remote.Name}' ...";
return Task.Run(() => {
if (_remote.Name != _name) {
return Task.Run(() =>
{
if (_remote.Name != _name)
{
var succ = new Commands.Remote(_repo.FullPath).Rename(_remote.Name, _name);
if (succ) _remote.Name = _name;
}
if (_remote.URL != _url) {
if (_remote.URL != _url)
{
var succ = new Commands.Remote(_repo.FullPath).SetURL(_name, _url);
if (succ) _remote.URL = _url;
}
if (_useSSH) {
if (_useSSH)
{
SetProgressDescription("Post processing ...");
new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", SSHKey);
}
@ -91,10 +111,10 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private Models.Remote _remote = null;
private readonly Repository _repo = null;
private readonly Models.Remote _remote = null;
private string _name = string.Empty;
private string _url = string.Empty;
private bool _useSSH = false;
}
}
}

View file

@ -2,35 +2,43 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class EditRepositoryNode : Popup {
public RepositoryNode Node {
namespace SourceGit.ViewModels
{
public class EditRepositoryNode : Popup
{
public RepositoryNode Node
{
get => _node;
set => SetProperty(ref _node, value);
}
public string Id {
public string Id
{
get => _id;
set => SetProperty(ref _id, value);
}
[Required(ErrorMessage = "Name is required!")]
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
public int Bookmark {
public int Bookmark
{
get => _bookmark;
set => SetProperty(ref _bookmark, value);
}
public bool IsRepository {
public bool IsRepository
{
get => _isRepository;
set => SetProperty(ref _isRepository, value);
}
public EditRepositoryNode(RepositoryNode node) {
public EditRepositoryNode(RepositoryNode node)
{
_node = node;
_id = node.Id;
_name = node.Name;
@ -40,16 +48,17 @@ namespace SourceGit.ViewModels {
View = new Views.EditRepositoryNode() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_node.Name = _name;
_node.Bookmark = _bookmark;
return null;
}
private RepositoryNode _node = null;
private RepositoryNode _node = null;
private string _id = string.Empty;
private string _name = string.Empty;
private bool _isRepository = false;
private int _bookmark = 0;
}
}
}

View file

@ -1,35 +1,42 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class FastForwardWithoutCheckout : Popup {
public Models.Branch Local {
namespace SourceGit.ViewModels
{
public class FastForwardWithoutCheckout : Popup
{
public Models.Branch Local
{
get;
private set;
}
public Models.Branch To {
public Models.Branch To
{
get;
private set;
}
public FastForwardWithoutCheckout(Repository repo, Models.Branch local, Models.Branch upstream) {
public FastForwardWithoutCheckout(Repository repo, Models.Branch local, Models.Branch upstream)
{
_repo = repo;
Local = local;
To = upstream;
View = new Views.FastForwardWithoutCheckout() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Fast-Forward ...";
return Task.Run(() => {
return Task.Run(() =>
{
new Commands.Fetch(_repo.FullPath, To.Remote, Local.Name, To.Name, SetProgressDescription).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,28 +1,35 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Fetch : Popup {
public List<Models.Remote> Remotes {
namespace SourceGit.ViewModels
{
public class Fetch : Popup
{
public List<Models.Remote> Remotes
{
get => _repo.Remotes;
}
public bool FetchAllRemotes {
public bool FetchAllRemotes
{
get => _fetchAllRemotes;
set => SetProperty(ref _fetchAllRemotes, value);
}
public Models.Remote SelectedRemote {
public Models.Remote SelectedRemote
{
get;
set;
}
public bool Prune {
public bool Prune
{
get;
set;
}
public Fetch(Repository repo, Models.Remote preferedRemote = null) {
public Fetch(Repository repo, Models.Remote preferedRemote = null)
{
_repo = repo;
_fetchAllRemotes = preferedRemote == null;
SelectedRemote = preferedRemote != null ? preferedRemote : _repo.Remotes[0];
@ -30,15 +37,21 @@ namespace SourceGit.ViewModels {
View = new Views.Fetch() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
return Task.Run(() => {
if (FetchAllRemotes) {
foreach (var remote in _repo.Remotes) {
return Task.Run(() =>
{
if (FetchAllRemotes)
{
foreach (var remote in _repo.Remotes)
{
SetProgressDescription($"Fetching remote: {remote.Name}");
new Commands.Fetch(_repo.FullPath, remote.Name, Prune, SetProgressDescription).Exec();
}
} else {
}
else
{
SetProgressDescription($"Fetching remote: {SelectedRemote.Name}");
new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, Prune, SetProgressDescription).Exec();
}
@ -48,7 +61,7 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private bool _fetchAllRemotes = true;
}
}
}

View file

@ -1,49 +1,66 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class FileHistories : ObservableObject {
public string File {
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class FileHistories : ObservableObject
{
public string File
{
get => _file;
}
public bool IsLoading {
public bool IsLoading
{
get => _isLoading;
private set => SetProperty(ref _isLoading, value);
}
public List<Models.Commit> Commits {
public List<Models.Commit> Commits
{
get => _commits;
set => SetProperty(ref _commits, value);
}
public Models.Commit SelectedCommit {
public Models.Commit SelectedCommit
{
get => _selectedCommit;
set {
if (SetProperty(ref _selectedCommit, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedCommit, value))
{
if (value == null)
{
DiffContext = null;
} else {
}
else
{
DiffContext = new DiffContext(_repo, new Models.DiffOption(value, _file));
}
}
}
}
public DiffContext DiffContext {
public DiffContext DiffContext
{
get => _diffContext;
set => SetProperty(ref _diffContext, value);
}
public FileHistories(string repo, string file) {
public FileHistories(string repo, string file)
{
_repo = repo;
_file = file;
Task.Run(() => {
Task.Run(() =>
{
var commits = new Commands.QueryCommits(_repo, $"-n 10000 -- \"{file}\"").Result();
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
IsLoading = false;
Commits = commits;
if (commits.Count > 0) SelectedCommit = commits[0];
@ -51,16 +68,17 @@ namespace SourceGit.ViewModels {
});
}
public void NavigateToCommit(string commitSHA) {
public void NavigateToCommit(string commitSHA)
{
var repo = Preference.FindRepository(_repo);
if (repo != null) repo.NavigateToCommit(commitSHA);
}
private string _repo = string.Empty;
private string _file = string.Empty;
private readonly string _repo = string.Empty;
private readonly string _file = string.Empty;
private bool _isLoading = true;
private List<Models.Commit> _commits = null;
private Models.Commit _selectedCommit = null;
private DiffContext _diffContext = null;
}
}
}

View file

@ -1,43 +1,58 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
using System.Collections.Generic;
namespace SourceGit.ViewModels {
public class FileTreeNode : ObservableObject {
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class FileTreeNode : ObservableObject
{
public string FullPath { get; set; } = string.Empty;
public bool IsFolder { get; set; } = false;
public object Backend { get; set; } = null;
public List<FileTreeNode> Children { get; set; } = new List<FileTreeNode>();
public bool IsExpanded {
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public static List<FileTreeNode> Build(List<Models.Change> changes) {
public static List<FileTreeNode> Build(List<Models.Change> changes)
{
var nodes = new List<FileTreeNode>();
var folders = new Dictionary<string, FileTreeNode>();
var expanded = changes.Count <= 50;
foreach (var c in changes) {
foreach (var c in changes)
{
var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1) {
nodes.Add(new FileTreeNode() {
if (sepIdx == -1)
{
nodes.Add(new FileTreeNode()
{
FullPath = c.Path,
Backend = c,
IsFolder = false,
IsExpanded = false
});
} else {
}
else
{
FileTreeNode lastFolder = null;
var start = 0;
while (sepIdx != -1) {
while (sepIdx != -1)
{
var folder = c.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
if (folders.ContainsKey(folder))
{
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new FileTreeNode() {
}
else if (lastFolder == null)
{
lastFolder = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
@ -45,8 +60,11 @@ namespace SourceGit.ViewModels {
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new FileTreeNode() {
}
else
{
var cur = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
@ -61,7 +79,8 @@ namespace SourceGit.ViewModels {
sepIdx = c.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileTreeNode() {
lastFolder.Children.Add(new FileTreeNode()
{
FullPath = c.Path,
Backend = c,
IsFolder = false,
@ -75,30 +94,41 @@ namespace SourceGit.ViewModels {
return nodes;
}
public static List<FileTreeNode> Build(List<Models.Object> files) {
public static List<FileTreeNode> Build(List<Models.Object> files)
{
var nodes = new List<FileTreeNode>();
var folders = new Dictionary<string, FileTreeNode>();
var expanded = files.Count <= 50;
foreach (var f in files) {
foreach (var f in files)
{
var sepIdx = f.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1) {
nodes.Add(new FileTreeNode() {
if (sepIdx == -1)
{
nodes.Add(new FileTreeNode()
{
FullPath = f.Path,
Backend = f,
IsFolder = false,
IsExpanded = false
});
} else {
}
else
{
FileTreeNode lastFolder = null;
var start = 0;
while (sepIdx != -1) {
while (sepIdx != -1)
{
var folder = f.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
if (folders.ContainsKey(folder))
{
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new FileTreeNode() {
}
else if (lastFolder == null)
{
lastFolder = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
@ -106,8 +136,11 @@ namespace SourceGit.ViewModels {
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new FileTreeNode() {
}
else
{
var cur = new FileTreeNode()
{
FullPath = folder,
Backend = null,
IsFolder = true,
@ -122,7 +155,8 @@ namespace SourceGit.ViewModels {
sepIdx = f.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileTreeNode() {
lastFolder.Children.Add(new FileTreeNode()
{
FullPath = f.Path,
Backend = f,
IsFolder = false,
@ -136,13 +170,17 @@ namespace SourceGit.ViewModels {
return nodes;
}
public static FileTreeNode SelectByPath(List<FileTreeNode> nodes, string path) {
foreach (var node in nodes) {
public static FileTreeNode SelectByPath(List<FileTreeNode> nodes, string path)
{
foreach (var node in nodes)
{
if (node.FullPath == path) return node;
if (node.IsFolder && path.StartsWith(node.FullPath + "/", StringComparison.Ordinal)) {
if (node.IsFolder && path.StartsWith(node.FullPath + "/", StringComparison.Ordinal))
{
var foundInChildren = SelectByPath(node.Children, path);
if (foundInChildren != null) {
if (foundInChildren != null)
{
node.IsExpanded = true;
}
return foundInChildren;
@ -152,20 +190,26 @@ namespace SourceGit.ViewModels {
return null;
}
private static void Sort(List<FileTreeNode> nodes) {
nodes.Sort((l, r) => {
if (l.IsFolder == r.IsFolder) {
private static void Sort(List<FileTreeNode> nodes)
{
nodes.Sort((l, r) =>
{
if (l.IsFolder == r.IsFolder)
{
return l.FullPath.CompareTo(r.FullPath);
} else {
}
else
{
return l.IsFolder ? -1 : 1;
}
});
foreach (var node in nodes) {
foreach (var node in nodes)
{
if (node.Children.Count > 1) Sort(node.Children);
}
}
private bool _isExpanded = true;
}
}
}

View file

@ -1,38 +1,45 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class GitFlowFinish : Popup {
namespace SourceGit.ViewModels
{
public class GitFlowFinish : Popup
{
public Models.Branch Branch => _branch;
public bool IsFeature => _type == Models.GitFlowBranchType.Feature;
public bool IsRelease => _type == Models.GitFlowBranchType.Release;
public bool IsHotfix => _type == Models.GitFlowBranchType.Hotfix;
public bool KeepBranch {
public bool KeepBranch
{
get;
set;
} = false;
public GitFlowFinish(Repository repo, Models.Branch branch, Models.GitFlowBranchType type) {
public GitFlowFinish(Repository repo, Models.Branch branch, Models.GitFlowBranchType type)
{
_repo = repo;
_branch = branch;
_type = type;
View = new Views.GitFlowFinish() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
return Task.Run(() => {
return Task.Run(() =>
{
var branch = _branch.Name;
switch (_type) {
case Models.GitFlowBranchType.Feature:
branch = branch.Substring(_repo.GitFlow.Feature.Length);
break;
case Models.GitFlowBranchType.Release:
branch = branch.Substring(_repo.GitFlow.Release.Length);
break;
default:
branch = branch.Substring(_repo.GitFlow.Hotfix.Length);
break;
switch (_type)
{
case Models.GitFlowBranchType.Feature:
branch = branch.Substring(_repo.GitFlow.Feature.Length);
break;
case Models.GitFlowBranchType.Release:
branch = branch.Substring(_repo.GitFlow.Release.Length);
break;
default:
branch = branch.Substring(_repo.GitFlow.Hotfix.Length);
break;
}
var succ = new Commands.GitFlow(_repo.FullPath).Finish(_type, branch, KeepBranch);
@ -41,8 +48,8 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private Models.Branch _branch = null;
private Models.GitFlowBranchType _type = Models.GitFlowBranchType.None;
private readonly Repository _repo = null;
private readonly Models.Branch _branch = null;
private readonly Models.GitFlowBranchType _type = Models.GitFlowBranchType.None;
}
}
}

View file

@ -1,17 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class GitFlowStart : Popup {
namespace SourceGit.ViewModels
{
public class GitFlowStart : Popup
{
[Required(ErrorMessage = "Name is required!!!")]
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
[CustomValidation(typeof(GitFlowStart), nameof(ValidateBranchName))]
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
public string Prefix {
public string Prefix
{
get => _prefix;
}
@ -19,29 +23,34 @@ namespace SourceGit.ViewModels {
public bool IsRelease => _type == Models.GitFlowBranchType.Release;
public bool IsHotfix => _type == Models.GitFlowBranchType.Hotfix;
public GitFlowStart(Repository repo, Models.GitFlowBranchType type) {
public GitFlowStart(Repository repo, Models.GitFlowBranchType type)
{
_repo = repo;
_type = type;
switch (type) {
case Models.GitFlowBranchType.Feature:
_prefix = repo.GitFlow.Feature;
break;
case Models.GitFlowBranchType.Release:
_prefix = repo.GitFlow.Release;
break;
default:
_prefix = repo.GitFlow.Hotfix;
break;
switch (type)
{
case Models.GitFlowBranchType.Feature:
_prefix = repo.GitFlow.Feature;
break;
case Models.GitFlowBranchType.Release:
_prefix = repo.GitFlow.Release;
break;
default:
_prefix = repo.GitFlow.Hotfix;
break;
}
View = new Views.GitFlowStart() { DataContext = this };
}
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) {
if (ctx.ObjectInstance is GitFlowStart starter) {
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx)
{
if (ctx.ObjectInstance is GitFlowStart starter)
{
var check = $"{starter._prefix}{name}";
foreach (var b in starter._repo.Branches) {
foreach (var b in starter._repo.Branches)
{
var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}";
if (test == check) return new ValidationResult("A branch with same name already exists!");
}
@ -50,18 +59,20 @@ namespace SourceGit.ViewModels {
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.GitFlow(_repo.FullPath).Start(_type, _name);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private Models.GitFlowBranchType _type = Models.GitFlowBranchType.Feature;
private string _prefix = string.Empty;
private readonly Repository _repo = null;
private readonly Models.GitFlowBranchType _type = Models.GitFlowBranchType.Feature;
private readonly string _prefix = string.Empty;
private string _name = null;
}
}
}

View file

@ -1,35 +1,47 @@
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class CountSelectedCommits {
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class CountSelectedCommits
{
public int Count { get; set; }
}
public class Histories : ObservableObject {
public bool IsLoading {
public class Histories : ObservableObject
{
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public double DataGridRowHeight {
public double DataGridRowHeight
{
get => _dataGridRowHeight;
}
public List<Models.Commit> Commits {
public List<Models.Commit> Commits
{
get => _commits;
set {
if (SetProperty(ref _commits, value)) {
set
{
if (SetProperty(ref _commits, value))
{
Graph = null;
Task.Run(() => {
Task.Run(() =>
{
var graph = Models.CommitGraph.Parse(value, DataGridRowHeight, 8);
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
Graph = graph;
});
});
@ -37,49 +49,62 @@ namespace SourceGit.ViewModels {
}
}
public Models.CommitGraph Graph {
public Models.CommitGraph Graph
{
get => _graph;
set => SetProperty(ref _graph, value);
}
public Models.Commit AutoSelectedCommit {
public Models.Commit AutoSelectedCommit
{
get => _autoSelectedCommit;
private set => SetProperty(ref _autoSelectedCommit, value);
}
public object DetailContext {
public object DetailContext
{
get => _detailContext;
private set => SetProperty(ref _detailContext, value);
}
public Histories(Repository repo) {
public Histories(Repository repo)
{
_repo = repo;
}
public void Cleanup() {
public void Cleanup()
{
Commits = new List<Models.Commit>();
_repo = null;
_graph = null;
_autoSelectedCommit = null;
if (_detailContext is CommitDetail cd) {
if (_detailContext is CommitDetail cd)
{
cd.Cleanup();
} else if (_detailContext is RevisionCompare rc) {
}
else if (_detailContext is RevisionCompare rc)
{
rc.Cleanup();
}
_detailContext = null;
}
public void NavigateTo(string commitSHA) {
public void NavigateTo(string commitSHA)
{
var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal));
if (commit != null) {
if (commit != null)
{
AutoSelectedCommit = commit;
if (_detailContext is CommitDetail detail) {
if (_detailContext is CommitDetail detail)
{
detail.Commit = commit;
} else {
}
else
{
var commitDetail = new CommitDetail(_repo.FullPath);
commitDetail.Commit = commit;
DetailContext = commitDetail;
@ -87,30 +112,42 @@ namespace SourceGit.ViewModels {
}
}
public void Select(IList commits) {
if (commits.Count == 0) {
public void Select(IList commits)
{
if (commits.Count == 0)
{
DetailContext = null;
} else if (commits.Count == 1) {
}
else if (commits.Count == 1)
{
var commit = commits[0] as Models.Commit;
AutoSelectedCommit = commit;
if (_detailContext is CommitDetail detail) {
if (_detailContext is CommitDetail detail)
{
detail.Commit = commit;
} else {
}
else
{
var commitDetail = new CommitDetail(_repo.FullPath);
commitDetail.Commit = commit;
DetailContext = commitDetail;
}
} else if (commits.Count == 2) {
}
else if (commits.Count == 2)
{
var end = commits[0] as Models.Commit;
var start = commits[1] as Models.Commit;
DetailContext = new RevisionCompare(_repo.FullPath, start, end);
} else {
}
else
{
DetailContext = new CountSelectedCommits() { Count = commits.Count };
}
}
public ContextMenu MakeContextMenu() {
public ContextMenu MakeContextMenu()
{
var detail = _detailContext as CommitDetail;
if (detail == null) return null;
@ -121,17 +158,26 @@ namespace SourceGit.ViewModels {
var menu = new ContextMenu();
var tags = new List<Models.Tag>();
if (commit.HasDecorators) {
foreach (var d in commit.Decorators) {
if (d.Type == Models.DecoratorType.CurrentBranchHead) {
if (commit.HasDecorators)
{
foreach (var d in commit.Decorators)
{
if (d.Type == Models.DecoratorType.CurrentBranchHead)
{
FillCurrentBranchMenu(menu, current);
} else if (d.Type == Models.DecoratorType.LocalBranchHead) {
}
else if (d.Type == Models.DecoratorType.LocalBranchHead)
{
var b = _repo.Branches.Find(x => x.IsLocal && d.Name == x.Name);
FillOtherLocalBranchMenu(menu, b, current, commit.IsMerged);
} else if (d.Type == Models.DecoratorType.RemoteBranchHead) {
}
else if (d.Type == Models.DecoratorType.RemoteBranchHead)
{
var b = _repo.Branches.Find(x => !x.IsLocal && d.Name == $"{x.Remote}/{x.Name}");
FillRemoteBranchMenu(menu, b, current, commit.IsMerged);
} else if (d.Type == Models.DecoratorType.Tag) {
}
else if (d.Type == Models.DecoratorType.Tag)
{
var t = _repo.Tags.Find(x => x.Name == d.Name);
if (t != null) tags.Add(t);
}
@ -140,25 +186,31 @@ namespace SourceGit.ViewModels {
if (menu.Items.Count > 0) menu.Items.Add(new MenuItem() { Header = "-" });
}
if (tags.Count > 0) {
if (tags.Count > 0)
{
foreach (var tag in tags) FillTagMenu(menu, tag);
menu.Items.Add(new MenuItem() { Header = "-" });
}
if (current.Head != commit.SHA) {
if (current.Head != commit.SHA)
{
var reset = new MenuItem();
reset.Header = new Views.NameHighlightedTextBlock("CommitCM.Reset", current.Name);
reset.Icon = App.CreateMenuIcon("Icons.Reset");
reset.Click += (o, e) => {
reset.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Reset(_repo, current, commit));
e.Handled = true;
};
menu.Items.Add(reset);
} else {
}
else
{
var reword = new MenuItem();
reword.Header = App.Text("CommitCM.Reword");
reword.Icon = App.CreateMenuIcon("Icons.Edit");
reword.Click += (o, e) => {
reword.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Reword(_repo, commit));
e.Handled = true;
};
@ -168,22 +220,26 @@ namespace SourceGit.ViewModels {
squash.Header = App.Text("CommitCM.Squash");
squash.Icon = App.CreateMenuIcon("Icons.SquashIntoParent");
squash.IsEnabled = commit.Parents.Count == 1;
squash.Click += (o, e) => {
if (commit.Parents.Count == 1) {
squash.Click += (o, e) =>
{
if (commit.Parents.Count == 1)
{
var parent = _commits.Find(x => x.SHA == commit.Parents[0]);
if (parent != null && PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Squash(_repo, commit, parent));
}
e.Handled = true;
};
menu.Items.Add(squash);
}
if (!commit.IsMerged) {
if (!commit.IsMerged)
{
var rebase = new MenuItem();
rebase.Header = new Views.NameHighlightedTextBlock("CommitCM.Rebase", current.Name);
rebase.Icon = App.CreateMenuIcon("Icons.Rebase");
rebase.Click += (o, e) => {
rebase.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Rebase(_repo, current, commit));
e.Handled = true;
};
@ -192,16 +248,20 @@ namespace SourceGit.ViewModels {
var cherryPick = new MenuItem();
cherryPick.Header = App.Text("CommitCM.CherryPick");
cherryPick.Icon = App.CreateMenuIcon("Icons.CherryPick");
cherryPick.Click += (o, e) => {
cherryPick.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CherryPick(_repo, commit));
e.Handled = true;
};
menu.Items.Add(cherryPick);
} else {
}
else
{
var revert = new MenuItem();
revert.Header = App.Text("CommitCM.Revert");
revert.Icon = App.CreateMenuIcon("Icons.Undo");
revert.Click += (o, e) => {
revert.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Revert(_repo, commit));
e.Handled = true;
};
@ -213,7 +273,8 @@ namespace SourceGit.ViewModels {
var createBranch = new MenuItem();
createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add");
createBranch.Header = App.Text("CreateBranch");
createBranch.Click += (o, e) => {
createBranch.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateBranch(_repo, commit));
e.Handled = true;
};
@ -222,7 +283,8 @@ namespace SourceGit.ViewModels {
var createTag = new MenuItem();
createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add");
createTag.Header = App.Text("CreateTag");
createTag.Click += (o, e) => {
createTag.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateTag(_repo, commit));
e.Handled = true;
};
@ -232,13 +294,15 @@ namespace SourceGit.ViewModels {
var saveToPatch = new MenuItem();
saveToPatch.Icon = App.CreateMenuIcon("Icons.Diff");
saveToPatch.Header = App.Text("CommitCM.SaveAsPatch");
saveToPatch.Click += async (_, e) => {
saveToPatch.Click += async (_, e) =>
{
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) {
if (selected.Count == 1)
{
var succ = new Commands.FormatPatch(_repo.FullPath, commit.SHA, selected[0].Path.LocalPath).Exec();
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@ -250,7 +314,8 @@ namespace SourceGit.ViewModels {
var archive = new MenuItem();
archive.Icon = App.CreateMenuIcon("Icons.Archive");
archive.Header = App.Text("Archive");
archive.Click += (o, e) => {
archive.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Archive(_repo, commit));
e.Handled = true;
};
@ -260,7 +325,8 @@ namespace SourceGit.ViewModels {
var copySHA = new MenuItem();
copySHA.Header = App.Text("CommitCM.CopySHA");
copySHA.Icon = App.CreateMenuIcon("Icons.Copy");
copySHA.Click += (o, e) => {
copySHA.Click += (o, e) =>
{
App.CopyText(commit.SHA);
e.Handled = true;
};
@ -268,19 +334,22 @@ namespace SourceGit.ViewModels {
return menu;
}
private void FillCurrentBranchMenu(ContextMenu menu, Models.Branch current) {
private void FillCurrentBranchMenu(ContextMenu menu, Models.Branch current)
{
var submenu = new MenuItem();
submenu.Icon = App.CreateMenuIcon("Icons.Branch");
submenu.Header = current.Name;
if (!string.IsNullOrEmpty(current.Upstream)) {
if (!string.IsNullOrEmpty(current.Upstream))
{
var upstream = current.Upstream.Substring(13);
var fastForward = new MenuItem();
fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", upstream);
fastForward.Icon = App.CreateMenuIcon("Icons.FastForward");
fastForward.IsEnabled = !string.IsNullOrEmpty(current.UpstreamTrackStatus) && current.UpstreamTrackStatus.IndexOf('↑') < 0; ;
fastForward.Click += (o, e) => {
fastForward.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowAndStartPopup(new Merge(_repo, upstream, current.Name));
e.Handled = true;
};
@ -289,7 +358,8 @@ namespace SourceGit.ViewModels {
var pull = new MenuItem();
pull.Header = new Views.NameHighlightedTextBlock("BranchCM.Pull", upstream);
pull.Icon = App.CreateMenuIcon("Icons.Pull");
pull.Click += (o, e) => {
pull.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Pull(_repo, null));
e.Handled = true;
};
@ -300,7 +370,8 @@ namespace SourceGit.ViewModels {
push.Header = new Views.NameHighlightedTextBlock("BranchCM.Push", current.Name);
push.Icon = App.CreateMenuIcon("Icons.Push");
push.IsEnabled = _repo.Remotes.Count > 0;
push.Click += (o, e) => {
push.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Push(_repo, current));
e.Handled = true;
};
@ -308,11 +379,13 @@ namespace SourceGit.ViewModels {
submenu.Items.Add(new MenuItem() { Header = "-" });
var type = _repo.GitFlow.GetBranchType(current.Name);
if (type != Models.GitFlowBranchType.None) {
if (type != Models.GitFlowBranchType.None)
{
var finish = new MenuItem();
finish.Header = new Views.NameHighlightedTextBlock("BranchCM.Finish", current.Name);
finish.Icon = App.CreateMenuIcon("Icons.Flow");
finish.Click += (o, e) => {
finish.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new GitFlowFinish(_repo, current, type));
e.Handled = true;
};
@ -323,7 +396,8 @@ namespace SourceGit.ViewModels {
var rename = new MenuItem();
rename.Header = new Views.NameHighlightedTextBlock("BranchCM.Rename", current.Name);
rename.Icon = App.CreateMenuIcon("Icons.Rename");
rename.Click += (o, e) => {
rename.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new RenameBranch(_repo, current));
e.Handled = true;
};
@ -332,7 +406,8 @@ namespace SourceGit.ViewModels {
menu.Items.Add(submenu);
}
private void FillOtherLocalBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) {
private void FillOtherLocalBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged)
{
var submenu = new MenuItem();
submenu.Icon = App.CreateMenuIcon("Icons.Branch");
submenu.Header = branch.Name;
@ -340,7 +415,8 @@ namespace SourceGit.ViewModels {
var checkout = new MenuItem();
checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", branch.Name);
checkout.Icon = App.CreateMenuIcon("Icons.Check");
checkout.Click += (o, e) => {
checkout.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowAndStartPopup(new Checkout(_repo, branch.Name));
e.Handled = true;
};
@ -350,7 +426,8 @@ namespace SourceGit.ViewModels {
merge.Header = new Views.NameHighlightedTextBlock("BranchCM.Merge", branch.Name, current.Name);
merge.Icon = App.CreateMenuIcon("Icons.Merge");
merge.IsEnabled = !merged;
merge.Click += (o, e) => {
merge.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Merge(_repo, branch.Name, current.Name));
e.Handled = true;
};
@ -358,11 +435,13 @@ namespace SourceGit.ViewModels {
submenu.Items.Add(new MenuItem() { Header = "-" });
var type = _repo.GitFlow.GetBranchType(branch.Name);
if (type != Models.GitFlowBranchType.None) {
if (type != Models.GitFlowBranchType.None)
{
var finish = new MenuItem();
finish.Header = new Views.NameHighlightedTextBlock("BranchCM.Finish", branch.Name);
finish.Icon = App.CreateMenuIcon("Icons.Flow");
finish.Click += (o, e) => {
finish.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new GitFlowFinish(_repo, branch, type));
e.Handled = true;
};
@ -373,7 +452,8 @@ namespace SourceGit.ViewModels {
var rename = new MenuItem();
rename.Header = new Views.NameHighlightedTextBlock("BranchCM.Rename", branch.Name);
rename.Icon = App.CreateMenuIcon("Icons.Rename");
rename.Click += (o, e) => {
rename.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new RenameBranch(_repo, branch));
e.Handled = true;
};
@ -382,7 +462,8 @@ namespace SourceGit.ViewModels {
var delete = new MenuItem();
delete.Header = new Views.NameHighlightedTextBlock("BranchCM.Delete", branch.Name);
delete.Icon = App.CreateMenuIcon("Icons.Clear");
delete.Click += (o, e) => {
delete.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteBranch(_repo, branch));
e.Handled = true;
};
@ -391,7 +472,8 @@ namespace SourceGit.ViewModels {
menu.Items.Add(submenu);
}
private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) {
private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged)
{
var name = $"{branch.Remote}/{branch.Name}";
var submenu = new MenuItem();
@ -401,9 +483,12 @@ namespace SourceGit.ViewModels {
var checkout = new MenuItem();
checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", name);
checkout.Icon = App.CreateMenuIcon("Icons.Check");
checkout.Click += (o, e) => {
foreach (var b in _repo.Branches) {
if (b.IsLocal && b.Upstream == branch.FullName) {
checkout.Click += (o, e) =>
{
foreach (var b in _repo.Branches)
{
if (b.IsLocal && b.Upstream == branch.FullName)
{
if (b.IsCurrent) return;
if (PopupHost.CanCreatePopup()) PopupHost.ShowAndStartPopup(new Checkout(_repo, b.Name));
return;
@ -419,7 +504,8 @@ namespace SourceGit.ViewModels {
merge.Header = new Views.NameHighlightedTextBlock("BranchCM.Merge", name, current.Name);
merge.Icon = App.CreateMenuIcon("Icons.Merge");
merge.IsEnabled = !merged;
merge.Click += (o, e) => {
merge.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Merge(_repo, name, current.Name));
e.Handled = true;
};
@ -430,7 +516,8 @@ namespace SourceGit.ViewModels {
var delete = new MenuItem();
delete.Header = new Views.NameHighlightedTextBlock("BranchCM.Delete", name);
delete.Icon = App.CreateMenuIcon("Icons.Clear");
delete.Click += (o, e) => {
delete.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteBranch(_repo, branch));
e.Handled = true;
};
@ -439,7 +526,8 @@ namespace SourceGit.ViewModels {
menu.Items.Add(submenu);
}
private void FillTagMenu(ContextMenu menu, Models.Tag tag) {
private void FillTagMenu(ContextMenu menu, Models.Tag tag)
{
var submenu = new MenuItem();
submenu.Header = tag.Name;
submenu.Icon = App.CreateMenuIcon("Icons.Tag");
@ -449,7 +537,8 @@ namespace SourceGit.ViewModels {
push.Header = new Views.NameHighlightedTextBlock("TagCM.Push", tag.Name);
push.Icon = App.CreateMenuIcon("Icons.Push");
push.IsEnabled = _repo.Remotes.Count > 0;
push.Click += (o, e) => {
push.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new PushTag(_repo, tag));
e.Handled = true;
};
@ -458,7 +547,8 @@ namespace SourceGit.ViewModels {
var delete = new MenuItem();
delete.Header = new Views.NameHighlightedTextBlock("TagCM.Delete", tag.Name);
delete.Icon = App.CreateMenuIcon("Icons.Clear");
delete.Click += (o, e) => {
delete.Click += (o, e) =>
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteTag(_repo, tag));
e.Handled = true;
};
@ -468,11 +558,11 @@ namespace SourceGit.ViewModels {
}
private Repository _repo = null;
private double _dataGridRowHeight = 28;
private readonly double _dataGridRowHeight = 28;
private bool _isLoading = true;
private List<Models.Commit> _commits = new List<Models.Commit>();
private Models.CommitGraph _graph = null;
private Models.Commit _autoSelectedCommit = null;
private object _detailContext = null;
}
}
}

View file

@ -1,30 +1,38 @@
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Init : Popup {
public string TargetPath {
namespace SourceGit.ViewModels
{
public class Init : Popup
{
public string TargetPath
{
get => _targetPath;
set => SetProperty(ref _targetPath, value);
}
public Init(string path) {
public Init(string path)
{
TargetPath = path;
View = new Views.Init() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
ProgressDescription = $"Initialize git repository at: '{_targetPath}'";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Init(HostPageId, _targetPath).Exec();
if (!succ) return false;
var gitDir = Path.GetFullPath(Path.Combine(_targetPath, ".git"));
CallUIThread(() => {
CallUIThread(() =>
{
var repo = Preference.AddRepository(_targetPath, gitDir);
var node = new RepositoryNode() {
var node = new RepositoryNode()
{
Id = repo.FullPath,
Name = Path.GetFileName(repo.FullPath),
Bookmark = 0,
@ -39,4 +47,4 @@ namespace SourceGit.ViewModels {
private string _targetPath;
}
}
}

View file

@ -2,16 +2,19 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public partial class InitGitFlow : Popup {
namespace SourceGit.ViewModels
{
public partial class InitGitFlow : Popup
{
[GeneratedRegex(@"^[\w\-/\.]+$")]
private static partial Regex TAG_PREFIX();
[Required(ErrorMessage = "Master branch name is required!!!")]
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
[CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))]
public string Master {
public string Master
{
get => _master;
set => SetProperty(ref _master, value, true);
}
@ -19,66 +22,79 @@ namespace SourceGit.ViewModels {
[Required(ErrorMessage = "Develop branch name is required!!!")]
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
[CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))]
public string Develop {
public string Develop
{
get => _develop;
set => SetProperty(ref _develop, value, true);
}
[Required(ErrorMessage = "Feature prefix is required!!!")]
[RegularExpression(@"^[\w\-\.]+/$", ErrorMessage = "Bad feature prefix format!")]
public string FeturePrefix {
public string FeturePrefix
{
get => _featurePrefix;
set => SetProperty(ref _featurePrefix, value, true);
}
[Required(ErrorMessage = "Release prefix is required!!!")]
[RegularExpression(@"^[\w\-\.]+/$", ErrorMessage = "Bad release prefix format!")]
public string ReleasePrefix {
public string ReleasePrefix
{
get => _releasePrefix;
set => SetProperty(ref _releasePrefix, value, true);
}
[Required(ErrorMessage = "Hotfix prefix is required!!!")]
[RegularExpression(@"^[\w\-\.]+/$", ErrorMessage = "Bad hotfix prefix format!")]
public string HotfixPrefix {
public string HotfixPrefix
{
get => _hotfixPrefix;
set => SetProperty(ref _hotfixPrefix, value, true);
}
[CustomValidation(typeof(InitGitFlow), nameof(ValidateTagPrefix))]
public string TagPrefix {
public string TagPrefix
{
get => _tagPrefix;
set => SetProperty(ref _tagPrefix, value, true);
}
public InitGitFlow(Repository repo) {
public InitGitFlow(Repository repo)
{
_repo = repo;
View = new Views.InitGitFlow() { DataContext = this };
}
public static ValidationResult ValidateBaseBranch(string _, ValidationContext ctx) {
if (ctx.ObjectInstance is InitGitFlow initializer) {
public static ValidationResult ValidateBaseBranch(string _, ValidationContext ctx)
{
if (ctx.ObjectInstance is InitGitFlow initializer)
{
if (initializer._master == initializer._develop) return new ValidationResult("Develop branch has the same name with master branch!");
}
return ValidationResult.Success;
}
public static ValidationResult ValidateTagPrefix(string tagPrefix, ValidationContext ctx) {
if (!string.IsNullOrWhiteSpace(tagPrefix) && !TAG_PREFIX().IsMatch(tagPrefix)) {
public static ValidationResult ValidateTagPrefix(string tagPrefix, ValidationContext ctx)
{
if (!string.IsNullOrWhiteSpace(tagPrefix) && !TAG_PREFIX().IsMatch(tagPrefix))
{
return new ValidationResult("Bad tag prefix format!");
}
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Init git-flow ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.GitFlow(_repo.FullPath).Init(_repo.Branches, _master, _develop, _featurePrefix, _releasePrefix, _hotfixPrefix, _tagPrefix);
if (succ) {
if (succ)
{
_repo.GitFlow.Feature = _featurePrefix;
_repo.GitFlow.Release = _releasePrefix;
_repo.GitFlow.Hotfix = _hotfixPrefix;
@ -89,7 +105,7 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _master = "master";
private string _develop = "develop";
private string _featurePrefix = "feature/";
@ -97,4 +113,4 @@ namespace SourceGit.ViewModels {
private string _hotfixPrefix = "hotfix/";
private string _tagPrefix = string.Empty;
}
}
}

View file

@ -1,30 +1,41 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
using System.IO;
namespace SourceGit.ViewModels {
public class Launcher : ObservableObject {
public AvaloniaList<LauncherPage> Pages {
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Launcher : ObservableObject
{
public AvaloniaList<LauncherPage> Pages
{
get;
private set;
}
public LauncherPage ActivePage {
public LauncherPage ActivePage
{
get => _activePage;
set {
if (SetProperty(ref _activePage, value)) {
set
{
if (SetProperty(ref _activePage, value))
{
PopupHost.Active = value;
}
}
}
public Launcher() {
public Launcher()
{
Pages = new AvaloniaList<LauncherPage>();
AddNewTab();
if (Preference.Instance.RestoreTabs) {
foreach (var id in Preference.Instance.OpenedTabs) {
if (Preference.Instance.RestoreTabs)
{
foreach (var id in Preference.Instance.OpenedTabs)
{
var node = Preference.FindNode(id);
if (node == null) continue;
@ -32,17 +43,21 @@ namespace SourceGit.ViewModels {
}
var lastActiveIdx = Preference.Instance.LastActiveTabIdx;
if (lastActiveIdx >= 0 && lastActiveIdx < Pages.Count) {
if (lastActiveIdx >= 0 && lastActiveIdx < Pages.Count)
{
ActivePage = Pages[lastActiveIdx];
}
}
}
public void Quit() {
public void Quit()
{
Preference.Instance.OpenedTabs.Clear();
if (Preference.Instance.RestoreTabs) {
foreach (var page in Pages) {
if (Preference.Instance.RestoreTabs)
{
foreach (var page in Pages)
{
if (page.Node.IsRepository) Preference.Instance.OpenedTabs.Add(page.Node.Id);
}
}
@ -51,20 +66,23 @@ namespace SourceGit.ViewModels {
Preference.Save();
}
public void AddNewTab() {
public void AddNewTab()
{
var page = new LauncherPage();
Pages.Add(page);
ActivePage = page;
}
public void MoveTab(LauncherPage from, LauncherPage to) {
public void MoveTab(LauncherPage from, LauncherPage to)
{
var fromIdx = Pages.IndexOf(from);
var toIdx = Pages.IndexOf(to);
Pages.Move(fromIdx, toIdx);
ActivePage = from;
}
public void GotoNextTab() {
public void GotoNextTab()
{
if (Pages.Count == 1) return;
var activeIdx = Pages.IndexOf(_activePage);
@ -72,8 +90,10 @@ namespace SourceGit.ViewModels {
ActivePage = Pages[nextIdx];
}
public void CloseTab(object param) {
if (Pages.Count == 1) {
public void CloseTab(object param)
{
if (Pages.Count == 1)
{
App.Quit();
return;
}
@ -83,21 +103,29 @@ namespace SourceGit.ViewModels {
var removeIdx = Pages.IndexOf(page);
var activeIdx = Pages.IndexOf(_activePage);
if (removeIdx == activeIdx) {
if (removeIdx == Pages.Count - 1) {
if (removeIdx == activeIdx)
{
if (removeIdx == Pages.Count - 1)
{
ActivePage = Pages[removeIdx - 1];
} else {
}
else
{
ActivePage = Pages[removeIdx + 1];
}
CloseRepositoryInTab(page);
Pages.RemoveAt(removeIdx);
OnPropertyChanged(nameof(Pages));
} else if (removeIdx + 1 == activeIdx) {
}
else if (removeIdx + 1 == activeIdx)
{
CloseRepositoryInTab(page);
Pages.RemoveAt(removeIdx);
OnPropertyChanged(nameof(Pages));
} else {
}
else
{
CloseRepositoryInTab(page);
Pages.RemoveAt(removeIdx);
}
@ -105,7 +133,8 @@ namespace SourceGit.ViewModels {
GC.Collect();
}
public void CloseOtherTabs(object param) {
public void CloseOtherTabs(object param)
{
if (Pages.Count == 1) return;
var page = param as LauncherPage;
@ -113,7 +142,8 @@ namespace SourceGit.ViewModels {
ActivePage = page;
foreach (var one in Pages) {
foreach (var one in Pages)
{
if (one.Node.Id != page.Node.Id) CloseRepositoryInTab(one);
}
@ -123,17 +153,20 @@ namespace SourceGit.ViewModels {
GC.Collect();
}
public void CloseRightTabs(object param) {
public void CloseRightTabs(object param)
{
LauncherPage page = param as LauncherPage;
if (page == null) page = _activePage;
var endIdx = Pages.IndexOf(page);
var activeIdx = Pages.IndexOf(_activePage);
if (endIdx < activeIdx) {
if (endIdx < activeIdx)
{
ActivePage = page;
}
for (var i = Pages.Count - 1; i > endIdx; i--) {
for (var i = Pages.Count - 1; i > endIdx; i--)
{
CloseRepositoryInTab(Pages[i]);
Pages.Remove(Pages[i]);
}
@ -141,16 +174,20 @@ namespace SourceGit.ViewModels {
GC.Collect();
}
public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) {
foreach (var one in Pages) {
if (one.Node.Id == node.Id) {
public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page)
{
foreach (var one in Pages)
{
if (one.Node.Id == node.Id)
{
ActivePage = one;
return;
}
}
var repo = Preference.FindRepository(node.Id);
if (repo == null || !Path.Exists(repo.FullPath)) {
if (repo == null || !Path.Exists(repo.FullPath))
{
var ctx = page == null ? ActivePage.Node.Id : page.Node.Id;
App.RaiseException(ctx, "Repository does NOT exists any more. Please remove it.");
return;
@ -159,16 +196,22 @@ namespace SourceGit.ViewModels {
repo.Open();
Commands.AutoFetch.AddRepository(repo.FullPath);
if (page == null) {
if (ActivePage == null || ActivePage.Node.IsRepository) {
if (page == null)
{
if (ActivePage == null || ActivePage.Node.IsRepository)
{
page = new LauncherPage(node, repo);
Pages.Add(page);
} else {
}
else
{
page = ActivePage;
page.Node = node;
page.Data = repo;
}
} else {
}
else
{
page.Node = node;
page.Data = repo;
}
@ -176,8 +219,10 @@ namespace SourceGit.ViewModels {
ActivePage = page;
}
private void CloseRepositoryInTab(LauncherPage page) {
if (page.Data is Repository repo) {
private void CloseRepositoryInTab(LauncherPage page)
{
if (page.Data is Repository repo)
{
Commands.AutoFetch.RemoveRepository(repo.FullPath);
repo.Close();
}
@ -187,4 +232,4 @@ namespace SourceGit.ViewModels {
private LauncherPage _activePage = null;
}
}
}

View file

@ -1,25 +1,33 @@
using Avalonia.Collections;
using System;
using System;
namespace SourceGit.ViewModels {
public class LauncherPage : PopupHost {
public RepositoryNode Node {
using Avalonia.Collections;
namespace SourceGit.ViewModels
{
public class LauncherPage : PopupHost
{
public RepositoryNode Node
{
get => _node;
set => SetProperty(ref _node, value);
}
public object Data {
public object Data
{
get => _data;
set => SetProperty(ref _data, value);
}
public AvaloniaList<Models.Notification> Notifications {
public AvaloniaList<Models.Notification> Notifications
{
get;
set;
} = new AvaloniaList<Models.Notification>();
public LauncherPage() {
_node = new RepositoryNode() {
public LauncherPage()
{
_node = new RepositoryNode()
{
Id = Guid.NewGuid().ToString(),
Name = "WelcomePage",
Bookmark = 0,
@ -28,21 +36,26 @@ namespace SourceGit.ViewModels {
_data = new Welcome();
}
public LauncherPage(RepositoryNode node, Repository repo) {
public LauncherPage(RepositoryNode node, Repository repo)
{
_node = node;
_data = repo;
}
public override string GetId() {
public override string GetId()
{
return _node.Id;
}
public void CopyPath() {
public void CopyPath()
{
if (_node.IsRepository) App.CopyText(_node.Id);
}
public void DismissNotification(object param) {
if (param is Models.Notification notice) {
public void DismissNotification(object param)
{
if (param is Models.Notification notice)
{
Notifications.Remove(notice);
}
}
@ -50,4 +63,4 @@ namespace SourceGit.ViewModels {
private RepositoryNode _node = null;
private object _data = null;
}
}
}

View file

@ -1,41 +1,50 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class MergeMode {
namespace SourceGit.ViewModels
{
public class MergeMode
{
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public MergeMode(string n, string d, string a) {
public MergeMode(string n, string d, string a)
{
Name = n;
Desc = d;
Arg = a;
}
}
public class Merge : Popup {
public string Source {
public class Merge : Popup
{
public string Source
{
get;
private set;
}
public string Into {
public string Into
{
get;
private set;
}
public List<MergeMode> Modes {
public List<MergeMode> Modes
{
get;
private set;
}
public MergeMode SelectedMode {
public MergeMode SelectedMode
{
get;
set;
}
public Merge(Repository repo, string source, string into) {
public Merge(Repository repo, string source, string into)
{
_repo = repo;
Source = source;
Into = into;
@ -49,17 +58,19 @@ namespace SourceGit.ViewModels {
View = new Views.Merge() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Merging '{Source}' into '{Into}' ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Merge(_repo.FullPath, Source, SelectedMode.Arg, SetProgressDescription).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,51 +1,63 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Popup : ObservableValidator {
public string HostPageId {
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Popup : ObservableValidator
{
public string HostPageId
{
get;
set;
}
public object View {
public object View
{
get;
set;
}
public bool InProgress {
public bool InProgress
{
get => _inProgress;
set => SetProperty(ref _inProgress, value);
}
public string ProgressDescription {
public string ProgressDescription
{
get => _progressDescription;
set => SetProperty(ref _progressDescription, value);
}
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode")]
public bool Check() {
public bool Check()
{
if (HasErrors) return false;
ValidateAllProperties();
return !HasErrors;
}
public virtual Task<bool> Sure() {
public virtual Task<bool> Sure()
{
return null;
}
protected void CallUIThread(Action action) {
protected void CallUIThread(Action action)
{
Dispatcher.UIThread.Invoke(action);
}
protected void SetProgressDescription(string description) {
protected void SetProgressDescription(string description)
{
CallUIThread(() => ProgressDescription = description);
}
private bool _inProgress = false;
private string _progressDescription = string.Empty;
}
}
}

View file

@ -1,57 +1,74 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels {
public class PopupHost : ObservableObject {
public static PopupHost Active {
namespace SourceGit.ViewModels
{
public class PopupHost : ObservableObject
{
public static PopupHost Active
{
get;
set;
} = null;
public Popup Popup {
public Popup Popup
{
get => _popup;
set => SetProperty(ref _popup, value);
}
public static bool CanCreatePopup() {
public static bool CanCreatePopup()
{
return Active != null && (Active._popup == null || !Active._popup.InProgress);
}
public static void ShowPopup(Popup popup) {
public static void ShowPopup(Popup popup)
{
popup.HostPageId = Active.GetId();
Active.Popup = popup;
}
public static void ShowAndStartPopup(Popup popup) {
public static void ShowAndStartPopup(Popup popup)
{
var dumpPage = Active;
popup.HostPageId = dumpPage.GetId();
dumpPage.Popup = popup;
dumpPage.ProcessPopup();
}
public virtual string GetId() {
public virtual string GetId()
{
return string.Empty;
}
public async void ProcessPopup() {
if (_popup != null) {
public async void ProcessPopup()
{
if (_popup != null)
{
if (!_popup.Check()) return;
_popup.InProgress = true;
var task = _popup.Sure();
if (task != null) {
if (task != null)
{
var finished = await task;
if (finished) {
if (finished)
{
Popup = null;
} else {
}
else
{
_popup.InProgress = false;
}
} else {
}
else
{
Popup = null;
}
}
}
public void CancelPopup() {
public void CancelPopup()
{
if (_popup == null) return;
if (_popup.InProgress) return;
Popup = null;
@ -59,4 +76,4 @@ namespace SourceGit.ViewModels {
private Popup _popup = null;
}
}
}

View file

@ -1,23 +1,36 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SourceGit.ViewModels {
public class Preference : ObservableObject {
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Preference : ObservableObject
{
[JsonIgnore]
public static Preference Instance {
get {
if (_instance == null) {
if (!File.Exists(_savePath)) {
public static Preference Instance
{
get
{
if (_instance == null)
{
if (!File.Exists(_savePath))
{
_instance = new Preference();
} else {
try {
}
else
{
try
{
_instance = JsonSerializer.Deserialize(File.ReadAllText(_savePath), JsonSerializationCodeGen.Default.Preference);
} catch {
}
catch
{
_instance = new Preference();
}
}
@ -25,7 +38,8 @@ namespace SourceGit.ViewModels {
_instance.Repositories.RemoveAll(x => !Directory.Exists(x.FullPath));
if (!_instance.IsGitConfigured) {
if (!_instance.IsGitConfigured)
{
_instance.GitInstallPath = Native.OS.FindGitExecutable();
}
@ -33,109 +47,137 @@ namespace SourceGit.ViewModels {
}
}
public string Locale {
public string Locale
{
get => _locale;
set {
if (SetProperty(ref _locale, value)) {
set
{
if (SetProperty(ref _locale, value))
{
App.SetLocale(value);
}
}
}
public string Theme {
public string Theme
{
get => _theme;
set {
if (SetProperty(ref _theme, value)) {
set
{
if (SetProperty(ref _theme, value))
{
App.SetTheme(value);
}
}
}
public string AvatarServer {
public string AvatarServer
{
get => Models.AvatarManager.SelectedServer;
set {
if (Models.AvatarManager.SelectedServer != value) {
set
{
if (Models.AvatarManager.SelectedServer != value)
{
Models.AvatarManager.SelectedServer = value;
OnPropertyChanged(nameof(AvatarServer));
}
}
}
public int MaxHistoryCommits {
public int MaxHistoryCommits
{
get => _maxHistoryCommits;
set => SetProperty(ref _maxHistoryCommits, value);
}
public bool RestoreTabs {
public bool RestoreTabs
{
get => _restoreTabs;
set => SetProperty(ref _restoreTabs, value);
}
public bool UseFixedTabWidth {
public bool UseFixedTabWidth
{
get => _useFixedTabWidth;
set => SetProperty(ref _useFixedTabWidth, value);
}
public bool UseTwoColumnsLayoutInHistories {
public bool UseTwoColumnsLayoutInHistories
{
get => _useTwoColumnsLayoutInHistories;
set => SetProperty(ref _useTwoColumnsLayoutInHistories, value);
}
public bool UseCombinedTextDiff {
public bool UseCombinedTextDiff
{
get => _useCombinedTextDiff;
set => SetProperty(ref _useCombinedTextDiff, value);
}
public Models.ChangeViewMode UnstagedChangeViewMode {
public Models.ChangeViewMode UnstagedChangeViewMode
{
get => _unstagedChangeViewMode;
set => SetProperty(ref _unstagedChangeViewMode, value);
}
public Models.ChangeViewMode StagedChangeViewMode {
public Models.ChangeViewMode StagedChangeViewMode
{
get => _stagedChangeViewMode;
set => SetProperty(ref _stagedChangeViewMode, value);
}
public Models.ChangeViewMode CommitChangeViewMode {
public Models.ChangeViewMode CommitChangeViewMode
{
get => _commitChangeViewMode;
set => SetProperty(ref _commitChangeViewMode, value);
}
[JsonIgnore]
public bool IsGitConfigured {
public bool IsGitConfigured
{
get => !string.IsNullOrEmpty(GitInstallPath) && File.Exists(GitInstallPath);
}
public string GitInstallPath {
public string GitInstallPath
{
get => Native.OS.GitInstallPath;
set {
if (Native.OS.GitInstallPath != value) {
set
{
if (Native.OS.GitInstallPath != value)
{
Native.OS.GitInstallPath = value;
OnPropertyChanged(nameof(GitInstallPath));
}
}
}
public string GitDefaultCloneDir {
public string GitDefaultCloneDir
{
get => _gitDefaultCloneDir;
set => SetProperty(ref _gitDefaultCloneDir, value);
}
public bool GitAutoFetch {
public bool GitAutoFetch
{
get => Commands.AutoFetch.IsEnabled;
set {
if (Commands.AutoFetch.IsEnabled != value) {
set
{
if (Commands.AutoFetch.IsEnabled != value)
{
Commands.AutoFetch.IsEnabled = value;
OnPropertyChanged(nameof(GitAutoFetch));
}
}
}
public int ExternalMergeToolType {
public int ExternalMergeToolType
{
get => _externalMergeToolType;
set {
set
{
var changed = SetProperty(ref _externalMergeToolType, value);
if (changed && !OperatingSystem.IsWindows() && value > 0 && value < Models.ExternalMergeTools.Supported.Count) {
if (changed && !OperatingSystem.IsWindows() && value > 0 && value < Models.ExternalMergeTools.Supported.Count)
{
var tool = Models.ExternalMergeTools.Supported[value];
if (File.Exists(tool.Exec)) ExternalMergeToolPath = tool.Exec;
else ExternalMergeToolPath = string.Empty;
@ -143,65 +185,80 @@ namespace SourceGit.ViewModels {
}
}
public string ExternalMergeToolPath {
public string ExternalMergeToolPath
{
get => _externalMergeToolPath;
set => SetProperty(ref _externalMergeToolPath, value);
}
public string ExternalMergeToolCmd {
public string ExternalMergeToolCmd
{
get => _externalMergeToolCmd;
set => SetProperty(ref _externalMergeToolCmd, value);
}
public string ExternalMergeToolDiffCmd {
public string ExternalMergeToolDiffCmd
{
get => _externalMergeToolDiffCmd;
set => SetProperty(ref _externalMergeToolDiffCmd, value);
}
public List<Repository> Repositories {
public List<Repository> Repositories
{
get;
set;
} = new List<Repository>();
public AvaloniaList<RepositoryNode> RepositoryNodes {
public AvaloniaList<RepositoryNode> RepositoryNodes
{
get => _repositoryNodes;
set => SetProperty(ref _repositoryNodes, value);
}
public List<string> OpenedTabs {
public List<string> OpenedTabs
{
get;
set;
} = new List<string>();
public int LastActiveTabIdx {
public int LastActiveTabIdx
{
get;
set;
} = 0;
public static void AddNode(RepositoryNode node, RepositoryNode to = null) {
public static void AddNode(RepositoryNode node, RepositoryNode to = null)
{
var collection = to == null ? _instance._repositoryNodes : to.SubNodes;
var list = new List<RepositoryNode>();
list.AddRange(collection);
list.Add(node);
list.Sort((l, r) => {
if (l.IsRepository != r.IsRepository) {
list.Sort((l, r) =>
{
if (l.IsRepository != r.IsRepository)
{
return l.IsRepository ? 1 : -1;
} else {
}
else
{
return l.Name.CompareTo(r.Name);
}
});
collection.Clear();
foreach (var one in list) {
foreach (var one in list)
{
collection.Add(one);
}
}
public static RepositoryNode FindNode(string id) {
public static RepositoryNode FindNode(string id)
{
return FindNodeRecursive(id, _instance.RepositoryNodes);
}
public static void MoveNode(RepositoryNode node, RepositoryNode to = null) {
public static void MoveNode(RepositoryNode node, RepositoryNode to = null)
{
if (to == null && _instance._repositoryNodes.Contains(node)) return;
if (to != null && to.SubNodes.Contains(node)) return;
@ -209,26 +266,32 @@ namespace SourceGit.ViewModels {
AddNode(node, to);
}
public static void RemoveNode(RepositoryNode node) {
public static void RemoveNode(RepositoryNode node)
{
RemoveNodeRecursive(node, _instance._repositoryNodes);
}
public static Repository FindRepository(string path) {
foreach (var repo in _instance.Repositories) {
public static Repository FindRepository(string path)
{
foreach (var repo in _instance.Repositories)
{
if (repo.FullPath == path) return repo;
}
return null;
}
public static Repository AddRepository(string rootDir, string gitDir) {
public static Repository AddRepository(string rootDir, string gitDir)
{
var normalized = rootDir.Replace('\\', '/');
var repo = FindRepository(normalized);
if (repo != null) {
if (repo != null)
{
repo.GitDir = gitDir;
return repo;
}
repo = new Repository() {
repo = new Repository()
{
FullPath = normalized,
GitDir = gitDir
};
@ -237,7 +300,8 @@ namespace SourceGit.ViewModels {
return repo;
}
public static void Save() {
public static void Save()
{
var dir = Path.GetDirectoryName(_savePath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
@ -245,8 +309,10 @@ namespace SourceGit.ViewModels {
File.WriteAllText(_savePath, data);
}
private static RepositoryNode FindNodeRecursive(string id, AvaloniaList<RepositoryNode> collection) {
foreach (var node in collection) {
private static RepositoryNode FindNodeRecursive(string id, AvaloniaList<RepositoryNode> collection)
{
foreach (var node in collection)
{
if (node.Id == id) return node;
var sub = FindNodeRecursive(id, node.SubNodes);
@ -256,13 +322,16 @@ namespace SourceGit.ViewModels {
return null;
}
private static bool RemoveNodeRecursive(RepositoryNode node, AvaloniaList<RepositoryNode> collection) {
if (collection.Contains(node)) {
private static bool RemoveNodeRecursive(RepositoryNode node, AvaloniaList<RepositoryNode> collection)
{
if (collection.Contains(node))
{
collection.Remove(node);
return true;
}
foreach (RepositoryNode one in collection) {
foreach (RepositoryNode one in collection)
{
if (RemoveNodeRecursive(node, one.SubNodes)) return true;
}

View file

@ -1,29 +1,35 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class PruneRemote : Popup {
public Models.Remote Remote {
namespace SourceGit.ViewModels
{
public class PruneRemote : Popup
{
public Models.Remote Remote
{
get;
private set;
}
public PruneRemote(Repository repo, Models.Remote remote) {
public PruneRemote(Repository repo, Models.Remote remote)
{
_repo = repo;
Remote = remote;
View = new Views.PruneRemote() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Run `prune` on remote ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Remote(_repo.FullPath).Prune(Remote.Name);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -2,22 +2,29 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Pull : Popup {
namespace SourceGit.ViewModels
{
public class Pull : Popup
{
public List<Models.Remote> Remotes => _repo.Remotes;
public Models.Branch Current => _current;
public bool HasSpecifiedRemoteBranch {
public bool HasSpecifiedRemoteBranch
{
get;
private set;
}
public Models.Remote SelectedRemote {
public Models.Remote SelectedRemote
{
get => _selectedRemote;
set {
if (SetProperty(ref _selectedRemote, value)) {
set
{
if (SetProperty(ref _selectedRemote, value))
{
var branches = new List<Models.Branch>();
foreach (var branch in _repo.Branches) {
foreach (var branch in _repo.Branches)
{
if (branch.Remote == value.Name) branches.Add(branch);
}
RemoteBranches = branches;
@ -26,81 +33,100 @@ namespace SourceGit.ViewModels {
}
}
public List<Models.Branch> RemoteBranches {
public List<Models.Branch> RemoteBranches
{
get => _remoteBranches;
private set => SetProperty(ref _remoteBranches, value);
}
[Required(ErrorMessage = "Remote branch to pull is required!!!")]
public Models.Branch SelectedBranch {
public Models.Branch SelectedBranch
{
get => _selectedBranch;
set => SetProperty(ref _selectedBranch, value);
}
public bool UseRebase {
public bool UseRebase
{
get;
set;
} = true;
public bool AutoStash {
public bool AutoStash
{
get;
set;
} = true;
public Pull(Repository repo, Models.Branch specifiedRemoteBranch) {
public Pull(Repository repo, Models.Branch specifiedRemoteBranch)
{
_repo = repo;
_current = repo.Branches.Find(x => x.IsCurrent);
if (specifiedRemoteBranch != null) {
if (specifiedRemoteBranch != null)
{
_selectedRemote = repo.Remotes.Find(x => x.Name == specifiedRemoteBranch.Remote);
_selectedBranch = specifiedRemoteBranch;
HasSpecifiedRemoteBranch = true;
} else {
if (!string.IsNullOrEmpty(_current.Upstream)) {
foreach (var branch in repo.Branches) {
if (!branch.IsLocal && _current.Upstream == branch.FullName) {
}
else
{
if (!string.IsNullOrEmpty(_current.Upstream))
{
foreach (var branch in repo.Branches)
{
if (!branch.IsLocal && _current.Upstream == branch.FullName)
{
_selectedRemote = repo.Remotes.Find(x => x.Name == branch.Remote);
_selectedBranch = branch;
break;
}
}
}
}
HasSpecifiedRemoteBranch = false;
}
// Make sure remote is exists.
if (_selectedRemote == null) {
if (_selectedRemote == null)
{
_selectedRemote = repo.Remotes[0];
_selectedBranch = null;
HasSpecifiedRemoteBranch = false;
}
_remoteBranches = new List<Models.Branch>();
foreach (var branch in _repo.Branches) {
foreach (var branch in _repo.Branches)
{
if (branch.Remote == _selectedRemote.Name) _remoteBranches.Add(branch);
}
if (_selectedBranch == null && _remoteBranches.Count > 0) {
if (_selectedBranch == null && _remoteBranches.Count > 0)
{
_selectedBranch = _remoteBranches[0];
}
View = new Views.Pull() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
return Task.Run(() => {
return Task.Run(() =>
{
var needPopStash = false;
if (AutoStash && _repo.WorkingCopyChangesCount > 0) {
if (AutoStash && _repo.WorkingCopyChangesCount > 0)
{
SetProgressDescription("Adding untracked changes...");
var succ = new Commands.Add(_repo.FullPath).Exec();
if (succ) {
if (succ)
{
SetProgressDescription("Stash local changes...");
succ = new Commands.Stash(_repo.FullPath).Push("PULL_AUTO_STASH");
}
if (!succ) {
if (!succ)
{
CallUIThread(() => _repo.SetWatcherEnabled(true));
return false;
}
@ -110,7 +136,8 @@ namespace SourceGit.ViewModels {
SetProgressDescription($"Pull {_selectedRemote.Name}/{_selectedBranch.Name}...");
var rs = new Commands.Pull(_repo.FullPath, _selectedRemote.Name, _selectedBranch.Name, UseRebase, SetProgressDescription).Exec();
if (rs && needPopStash) {
if (rs && needPopStash)
{
SetProgressDescription("Re-apply local changes...");
rs = new Commands.Stash(_repo.FullPath).Pop("stash@{0}");
}
@ -120,10 +147,10 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private Models.Branch _current = null;
private readonly Repository _repo = null;
private readonly Models.Branch _current = null;
private Models.Remote _selectedRemote = null;
private List<Models.Branch> _remoteBranches = null;
private Models.Branch _selectedBranch = null;
}
}
}

View file

@ -2,24 +2,33 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Push : Popup {
public bool HasSpecifiedLocalBranch {
namespace SourceGit.ViewModels
{
public class Push : Popup
{
public bool HasSpecifiedLocalBranch
{
get;
private set;
}
[Required(ErrorMessage = "Local branch is required!!!")]
public Models.Branch SelectedLocalBranch {
public Models.Branch SelectedLocalBranch
{
get => _selectedLocalBranch;
set {
if (SetProperty(ref _selectedLocalBranch, value)) {
set
{
if (SetProperty(ref _selectedLocalBranch, value))
{
// If selected local branch has upstream branch. Try to find it's remote.
if (!string.IsNullOrEmpty(value.Upstream)) {
if (!string.IsNullOrEmpty(value.Upstream))
{
var branch = _repo.Branches.Find(x => x.FullName == value.Upstream);
if (branch != null) {
if (branch != null)
{
var remote = _repo.Remotes.Find(x => x.Name == branch.Remote);
if (remote != null && remote != _selectedRemote) {
if (remote != null && remote != _selectedRemote)
{
SelectedRemote = remote;
return;
}
@ -32,68 +41,84 @@ namespace SourceGit.ViewModels {
}
}
public List<Models.Branch> LocalBranches {
public List<Models.Branch> LocalBranches
{
get;
private set;
}
public List<Models.Remote> Remotes {
public List<Models.Remote> Remotes
{
get => _repo.Remotes;
}
[Required(ErrorMessage = "Remote is required!!!")]
public Models.Remote SelectedRemote {
public Models.Remote SelectedRemote
{
get => _selectedRemote;
set {
set
{
if (SetProperty(ref _selectedRemote, value)) AutoSelectBranchByRemote();
}
}
public List<Models.Branch> RemoteBranches {
public List<Models.Branch> RemoteBranches
{
get => _remoteBranches;
private set => SetProperty(ref _remoteBranches, value);
}
[Required(ErrorMessage = "Remote branch is required!!!")]
public Models.Branch SelectedRemoteBranch {
public Models.Branch SelectedRemoteBranch
{
get => _selectedRemoteBranch;
set => SetProperty(ref _selectedRemoteBranch, value);
}
public bool PushAllTags {
public bool PushAllTags
{
get;
set;
}
public bool ForcePush {
public bool ForcePush
{
get;
set;
}
public Push(Repository repo, Models.Branch localBranch) {
public Push(Repository repo, Models.Branch localBranch)
{
_repo = repo;
// Gather all local branches and find current branch.
LocalBranches = new List<Models.Branch>();
var current = null as Models.Branch;
foreach (var branch in _repo.Branches) {
foreach (var branch in _repo.Branches)
{
if (branch.IsLocal) LocalBranches.Add(branch);
if (branch.IsCurrent) current = branch;
}
// Set default selected local branch.
if (localBranch != null) {
if (localBranch != null)
{
_selectedLocalBranch = localBranch;
HasSpecifiedLocalBranch = true;
} else {
}
else
{
_selectedLocalBranch = current;
HasSpecifiedLocalBranch = false;
}
// Find preferred remote if selected local branch has upstream.
if (!string.IsNullOrEmpty(_selectedLocalBranch.Upstream)) {
foreach (var branch in repo.Branches) {
if (!branch.IsLocal && _selectedLocalBranch.Upstream == branch.FullName) {
if (!string.IsNullOrEmpty(_selectedLocalBranch.Upstream))
{
foreach (var branch in repo.Branches)
{
if (!branch.IsLocal && _selectedLocalBranch.Upstream == branch.FullName)
{
_selectedRemote = repo.Remotes.Find(x => x.Name == branch.Remote);
break;
}
@ -109,13 +134,15 @@ namespace SourceGit.ViewModels {
View = new Views.Push() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
var remoteBranchName = _selectedRemoteBranch.Name.Replace(" (new)", "");
ProgressDescription = $"Push {_selectedLocalBranch.Name} -> {_selectedRemote.Name}/{remoteBranchName} ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Push(
_repo.FullPath,
_selectedLocalBranch.Name,
@ -130,17 +157,22 @@ namespace SourceGit.ViewModels {
});
}
private void AutoSelectBranchByRemote() {
private void AutoSelectBranchByRemote()
{
// Gather branches.
var branches = new List<Models.Branch>();
foreach (var branch in _repo.Branches) {
foreach (var branch in _repo.Branches)
{
if (branch.Remote == _selectedRemote.Name) branches.Add(branch);
}
// If selected local branch has upstream branch. Try to find it in current remote branches.
if (!string.IsNullOrEmpty(_selectedLocalBranch.Upstream)) {
foreach (var branch in branches) {
if (_selectedLocalBranch.Upstream == branch.FullName) {
if (!string.IsNullOrEmpty(_selectedLocalBranch.Upstream))
{
foreach (var branch in branches)
{
if (_selectedLocalBranch.Upstream == branch.FullName)
{
RemoteBranches = branches;
SelectedRemoteBranch = branch;
return;
@ -149,8 +181,10 @@ namespace SourceGit.ViewModels {
}
// Find best remote branch by name.
foreach (var branch in branches) {
if (_selectedLocalBranch.Name == branch.Name) {
foreach (var branch in branches)
{
if (_selectedLocalBranch.Name == branch.Name)
{
RemoteBranches = branches;
SelectedRemoteBranch = branch;
return;
@ -158,7 +192,8 @@ namespace SourceGit.ViewModels {
}
// Add a fake new branch.
var fake = new Models.Branch() {
var fake = new Models.Branch()
{
Name = $"{_selectedLocalBranch.Name} (new)",
Remote = _selectedRemote.Name,
};
@ -167,10 +202,10 @@ namespace SourceGit.ViewModels {
SelectedRemoteBranch = fake;
}
private Repository _repo = null;
private readonly Repository _repo = null;
private Models.Branch _selectedLocalBranch = null;
private Models.Remote _selectedRemote = null;
private List<Models.Branch> _remoteBranches = new List<Models.Branch>();
private Models.Branch _selectedRemoteBranch = null;
}
}
}

View file

@ -1,40 +1,48 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class PushTag : Popup {
public Models.Tag Target {
namespace SourceGit.ViewModels
{
public class PushTag : Popup
{
public Models.Tag Target
{
get;
private set;
}
public List<Models.Remote> Remotes {
public List<Models.Remote> Remotes
{
get => _repo.Remotes;
}
public Models.Remote SelectedRemote {
public Models.Remote SelectedRemote
{
get;
set;
}
public PushTag(Repository repo, Models.Tag target) {
public PushTag(Repository repo, Models.Tag target)
{
_repo = repo;
Target = target;
SelectedRemote = _repo.Remotes[0];
View = new Views.PushTag() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Pushing tag '{Target.Name}' to remote '{SelectedRemote.Name}' ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Push(_repo.FullPath, SelectedRemote.Name, Target.Name, false).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,23 +1,29 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Rebase : Popup {
public Models.Branch Current {
namespace SourceGit.ViewModels
{
public class Rebase : Popup
{
public Models.Branch Current
{
get;
private set;
}
public object On {
public object On
{
get;
private set;
}
public bool AutoStash {
public bool AutoStash
{
get;
set;
}
public Rebase(Repository repo, Models.Branch current, Models.Branch on) {
public Rebase(Repository repo, Models.Branch current, Models.Branch on)
{
_repo = repo;
_revision = on.Head;
Current = current;
@ -26,7 +32,8 @@ namespace SourceGit.ViewModels {
View = new Views.Rebase() { DataContext = this };
}
public Rebase(Repository repo, Models.Branch current, Models.Commit on) {
public Rebase(Repository repo, Models.Branch current, Models.Commit on)
{
_repo = repo;
_revision = on.SHA;
Current = current;
@ -35,18 +42,20 @@ namespace SourceGit.ViewModels {
View = new Views.Rebase() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Rebasing ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Rebase(_repo.FullPath, _revision, AutoStash).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private string _revision = string.Empty;
private readonly Repository _repo = null;
private readonly string _revision = string.Empty;
}
}
}

View file

@ -1,9 +1,12 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class RenameBranch : Popup {
public Models.Branch Target {
namespace SourceGit.ViewModels
{
public class RenameBranch : Popup
{
public Models.Branch Target
{
get;
private set;
}
@ -11,22 +14,28 @@ namespace SourceGit.ViewModels {
[Required(ErrorMessage = "Branch name is required!!!")]
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
[CustomValidation(typeof(RenameBranch), nameof(ValidateBranchName))]
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
public RenameBranch(Repository repo, Models.Branch target) {
public RenameBranch(Repository repo, Models.Branch target)
{
_repo = repo;
_name = target.Name;
Target = target;
View = new Views.RenameBranch() { DataContext = this };
}
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) {
if (ctx.ObjectInstance is RenameBranch rename) {
foreach (var b in rename._repo.Branches) {
if (b != rename.Target && b.Name == name) {
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx)
{
if (ctx.ObjectInstance is RenameBranch rename)
{
foreach (var b in rename._repo.Branches)
{
if (b != rename.Target && b.Name == name)
{
return new ValidationResult("A branch with same name already exists!!!");
}
}
@ -35,20 +44,22 @@ namespace SourceGit.ViewModels {
return ValidationResult.Success;
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
if (_name == Target.Name) return null;
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Rename '{Target.Name}'";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = Commands.Branch.Rename(_repo.FullPath, Target.Name, _name);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _name = string.Empty;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,34 +1,42 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class RepositoryConfigure : Popup {
public string UserName {
namespace SourceGit.ViewModels
{
public class RepositoryConfigure : Popup
{
public string UserName
{
get;
set;
}
public string UserEmail {
public string UserEmail
{
get;
set;
}
public bool GPGSigningEnabled {
public bool GPGSigningEnabled
{
get;
set;
}
public string GPGUserSigningKey {
public string GPGUserSigningKey
{
get;
set;
}
public string HttpProxy {
public string HttpProxy
{
get;
set;
}
public RepositoryConfigure(Repository repo) {
public RepositoryConfigure(Repository repo)
{
_repo = repo;
_cached = new Commands.Config(repo.FullPath).ListAll();
@ -41,7 +49,8 @@ namespace SourceGit.ViewModels {
View = new Views.RepositoryConfigure() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
SetIfChanged("user.name", UserName);
SetIfChanged("user.email", UserEmail);
SetIfChanged("commit.gpgsign", GPGSigningEnabled ? "true" : "false");
@ -50,20 +59,25 @@ namespace SourceGit.ViewModels {
return null;
}
private void SetIfChanged(string key, string value) {
private void SetIfChanged(string key, string value)
{
bool changed = false;
if (_cached.ContainsKey(key)) {
if (_cached.ContainsKey(key))
{
changed = value != _cached[key];
} else if (!string.IsNullOrEmpty(value)) {
}
else if (!string.IsNullOrEmpty(value))
{
changed = true;
}
if (changed) {
if (changed)
{
new Commands.Config(_repo.FullPath).Set(key, value);
}
}
private Repository _repo = null;
private Dictionary<string, string> _cached = null;
private readonly Repository _repo = null;
private readonly Dictionary<string, string> _cached = null;
}
}
}

View file

@ -1,67 +1,84 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace SourceGit.ViewModels {
public class RepositoryNode : ObservableObject {
public string Id {
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class RepositoryNode : ObservableObject
{
public string Id
{
get => _id;
set {
set
{
var normalized = value.Replace('\\', '/');
SetProperty(ref _id, normalized);
}
}
public string Name {
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public int Bookmark {
public int Bookmark
{
get => _bookmark;
set => SetProperty(ref _bookmark, value);
}
public bool IsRepository {
public bool IsRepository
{
get => _isRepository;
set => SetProperty(ref _isRepository, value);
}
public bool IsExpanded {
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
[JsonIgnore]
public bool IsVisible {
public bool IsVisible
{
get => _isVisible;
set => SetProperty(ref _isVisible, value);
}
public AvaloniaList<RepositoryNode> SubNodes {
public AvaloniaList<RepositoryNode> SubNodes
{
get => _subNodes;
set => SetProperty(ref _subNodes, value);
}
public void Edit() {
public void Edit()
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new EditRepositoryNode(this));
}
public void AddSubFolder() {
public void AddSubFolder()
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateGroup(this));
}
public void OpenInFileManager() {
public void OpenInFileManager()
{
if (!IsRepository) return;
Native.OS.OpenInFileManager(_id);
}
public void OpenTerminal() {
public void OpenTerminal()
{
if (!IsRepository) return;
Native.OS.OpenTerminal(_id);
}
public void Delete() {
public void Delete()
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteRepositoryNode(this));
}
@ -73,4 +90,4 @@ namespace SourceGit.ViewModels {
private bool _isVisible = true;
private AvaloniaList<RepositoryNode> _subNodes = new AvaloniaList<RepositoryNode>();
}
}
}

View file

@ -1,15 +1,19 @@
using Avalonia.Media;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class ResetMode {
using Avalonia.Media;
namespace SourceGit.ViewModels
{
public class ResetMode
{
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
public IBrush Color { get; set; }
public ResetMode(string n, string d, string a, IBrush b) {
public ResetMode(string n, string d, string a, IBrush b)
{
Name = n;
Desc = d;
Arg = a;
@ -17,28 +21,34 @@ namespace SourceGit.ViewModels {
}
}
public class Reset : Popup {
public Models.Branch Current {
public class Reset : Popup
{
public Models.Branch Current
{
get;
private set;
}
public Models.Commit To {
public Models.Commit To
{
get;
private set;
}
public List<ResetMode> Modes {
public List<ResetMode> Modes
{
get;
private set;
}
public ResetMode SelectedMode {
public ResetMode SelectedMode
{
get;
set;
}
public Reset(Repository repo, Models.Branch current, Models.Commit to) {
public Reset(Repository repo, Models.Branch current, Models.Commit to)
{
_repo = repo;
Current = current;
To = to;
@ -51,17 +61,19 @@ namespace SourceGit.ViewModels {
View = new Views.Reset() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Reset current branch to {To.SHA} ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Reset(_repo.FullPath, To.SHA, SelectedMode.Arg).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,35 +1,42 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Revert : Popup {
public Models.Commit Target {
namespace SourceGit.ViewModels
{
public class Revert : Popup
{
public Models.Commit Target
{
get;
private set;
}
public bool AutoCommit {
public bool AutoCommit
{
get;
set;
}
public Revert(Repository repo, Models.Commit target) {
public Revert(Repository repo, Models.Commit target)
{
_repo = repo;
Target = target;
AutoCommit = true;
View = new Views.Revert() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Revert commit '{Target.SHA}' ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Revert(_repo.FullPath, Target.SHA, AutoCommit).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
}
}
}

View file

@ -1,41 +1,55 @@
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class RevisionCompare : ObservableObject {
public Models.Commit StartPoint {
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class RevisionCompare : ObservableObject
{
public Models.Commit StartPoint
{
get;
private set;
}
public Models.Commit EndPoint {
public Models.Commit EndPoint
{
get;
private set;
}
public List<Models.Change> VisibleChanges {
public List<Models.Change> VisibleChanges
{
get => _visibleChanges;
private set => SetProperty(ref _visibleChanges, value);
}
public List<FileTreeNode> ChangeTree {
public List<FileTreeNode> ChangeTree
{
get => _changeTree;
private set => SetProperty(ref _changeTree, value);
}
public Models.Change SelectedChange {
public Models.Change SelectedChange
{
get => _selectedChange;
set {
if (SetProperty(ref _selectedChange, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedChange, value))
{
if (value == null)
{
SelectedNode = null;
DiffContext = null;
} else {
}
else
{
SelectedNode = FileTreeNode.SelectByPath(_changeTree, value.Path);
DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, EndPoint.SHA, value));
}
@ -43,60 +57,77 @@ namespace SourceGit.ViewModels {
}
}
public FileTreeNode SelectedNode {
public FileTreeNode SelectedNode
{
get => _selectedNode;
set {
if (SetProperty(ref _selectedNode, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedNode, value))
{
if (value == null)
{
SelectedChange = null;
} else {
}
else
{
SelectedChange = value.Backend as Models.Change;
}
}
}
}
public string SearchFilter {
public string SearchFilter
{
get => _searchFilter;
set {
if (SetProperty(ref _searchFilter, value)) {
set
{
if (SetProperty(ref _searchFilter, value))
{
RefreshVisible();
}
}
}
public DiffContext DiffContext {
public DiffContext DiffContext
{
get => _diffContext;
private set => SetProperty(ref _diffContext, value);
}
public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endPoint) {
public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endPoint)
{
_repo = repo;
StartPoint = startPoint;
EndPoint = endPoint;
Task.Run(() => {
Task.Run(() =>
{
_changes = new Commands.CompareRevisions(_repo, startPoint.SHA, endPoint.SHA).Result();
var visible = _changes;
if (!string.IsNullOrWhiteSpace(_searchFilter)) {
if (!string.IsNullOrWhiteSpace(_searchFilter))
{
visible = new List<Models.Change>();
foreach (var c in _changes) {
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) {
foreach (var c in _changes)
{
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(c);
}
}
}
var tree = FileTreeNode.Build(visible);
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
VisibleChanges = visible;
ChangeTree = tree;
});
});
}
public void Cleanup() {
public void Cleanup()
{
_repo = null;
if (_changes != null) _changes.Clear();
if (_visibleChanges != null) _visibleChanges.Clear();
@ -107,22 +138,27 @@ namespace SourceGit.ViewModels {
_diffContext = null;
}
public void NavigateTo(string commitSHA) {
public void NavigateTo(string commitSHA)
{
var repo = Preference.FindRepository(_repo);
if (repo != null) repo.NavigateToCommit(commitSHA);
}
public void ClearSearchFilter() {
public void ClearSearchFilter()
{
SearchFilter = string.Empty;
}
public ContextMenu CreateChangeContextMenu(Models.Change change) {
public ContextMenu CreateChangeContextMenu(Models.Change change)
{
var menu = new ContextMenu();
if (change.Index != Models.ChangeState.Deleted) {
if (change.Index != Models.ChangeState.Deleted)
{
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Click += (_, ev) => {
history.Click += (_, ev) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
window.Show();
ev.Handled = true;
@ -132,7 +168,8 @@ namespace SourceGit.ViewModels {
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.IsEnabled = File.Exists(full);
explore.Click += (_, ev) => {
explore.Click += (_, ev) =>
{
Native.OS.OpenInFileManager(full, true);
ev.Handled = true;
};
@ -143,7 +180,8 @@ namespace SourceGit.ViewModels {
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Click += (_, ev) => {
copyPath.Click += (_, ev) =>
{
App.CopyText(change.Path);
ev.Handled = true;
};
@ -152,15 +190,21 @@ namespace SourceGit.ViewModels {
return menu;
}
private void RefreshVisible() {
private void RefreshVisible()
{
if (_changes == null) return;
if (string.IsNullOrEmpty(_searchFilter)) {
if (string.IsNullOrEmpty(_searchFilter))
{
VisibleChanges = _changes;
} else {
}
else
{
var visible = new List<Models.Change>();
foreach (var c in _changes) {
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) {
foreach (var c in _changes)
{
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(c);
}
}
@ -180,4 +224,4 @@ namespace SourceGit.ViewModels {
private string _searchFilter = string.Empty;
private DiffContext _diffContext = null;
}
}
}

View file

@ -1,40 +1,47 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Reword : Popup {
public Models.Commit Head {
namespace SourceGit.ViewModels
{
public class Reword : Popup
{
public Models.Commit Head
{
get;
private set;
}
[Required(ErrorMessage = "Commit message is required!!!")]
public string Message {
public string Message
{
get => _message;
set => SetProperty(ref _message, value, true);
}
public Reword(Repository repo, Models.Commit head) {
public Reword(Repository repo, Models.Commit head)
{
_repo = repo;
Head = head;
Message = head.FullMessage;
View = new Views.Reword() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
if (_message == Head.FullMessage) return null;
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Editing head commit message ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Commit(_repo.FullPath, _message, true, true).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _message = string.Empty;
}
}
}

View file

@ -1,25 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Squash : Popup {
public Models.Commit Head {
namespace SourceGit.ViewModels
{
public class Squash : Popup
{
public Models.Commit Head
{
get;
private set;
}
public Models.Commit Parent {
public Models.Commit Parent
{
get;
private set;
}
[Required(ErrorMessage = "Commit message is required!!!")]
public string Message {
public string Message
{
get => _message;
set => SetProperty(ref _message, value, true);
}
public Squash(Repository repo, Models.Commit head, Models.Commit parent) {
public Squash(Repository repo, Models.Commit head, Models.Commit parent)
{
_repo = repo;
_message = parent.FullMessage;
Head = head;
@ -27,11 +33,13 @@ namespace SourceGit.ViewModels {
View = new Views.Squash() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Squashing ...";
return Task.Run(() => {
return Task.Run(() =>
{
var succ = new Commands.Reset(_repo.FullPath, Parent.SHA, "--soft").Exec();
if (succ) succ = new Commands.Commit(_repo.FullPath, _message, true).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true));
@ -39,7 +47,7 @@ namespace SourceGit.ViewModels {
});
}
private Repository _repo = null;
private readonly Repository _repo = null;
private string _message = string.Empty;
}
}
}

View file

@ -1,39 +1,49 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class StashChanges : Popup {
namespace SourceGit.ViewModels
{
public class StashChanges : Popup
{
public string Message {
public string Message
{
get;
set;
}
public bool CanIgnoreUntracked {
public bool CanIgnoreUntracked
{
get;
private set;
}
public bool IncludeUntracked {
public bool IncludeUntracked
{
get;
set;
}
public StashChanges(Repository repo, List<Models.Change> changes, bool canIgnoreUntracked) {
public StashChanges(Repository repo, List<Models.Change> changes, bool canIgnoreUntracked)
{
_repo = repo;
_changes = changes;
CanIgnoreUntracked = canIgnoreUntracked;
IncludeUntracked = true;
View = new Views.StashChanges() { DataContext = this };
}
public override Task<bool> Sure() {
public override Task<bool> Sure()
{
var jobs = _changes;
if (CanIgnoreUntracked && !IncludeUntracked) {
if (CanIgnoreUntracked && !IncludeUntracked)
{
jobs = new List<Models.Change>();
foreach (var job in _changes) {
if (job.WorkTree != Models.ChangeState.Untracked && job.WorkTree != Models.ChangeState.Added) {
foreach (var job in _changes)
{
if (job.WorkTree != Models.ChangeState.Untracked && job.WorkTree != Models.ChangeState.Added)
{
jobs.Add(job);
}
}
@ -44,14 +54,15 @@ namespace SourceGit.ViewModels {
_repo.SetWatcherEnabled(false);
ProgressDescription = $"Stash changes ...";
return Task.Run(() => {
return Task.Run(() =>
{
new Commands.Stash(_repo.FullPath).Push(jobs, Message);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private Repository _repo = null;
private List<Models.Change> _changes = null;
private readonly Repository _repo = null;
private readonly List<Models.Change> _changes = null;
}
}
}

View file

@ -1,33 +1,49 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class StashesPage : ObservableObject {
public int Count {
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class StashesPage : ObservableObject
{
public int Count
{
get => _stashes == null ? 0 : _stashes.Count;
}
public List<Models.Stash> Stashes {
public List<Models.Stash> Stashes
{
get => _stashes;
set {
if (SetProperty(ref _stashes, value)) {
set
{
if (SetProperty(ref _stashes, value))
{
SelectedStash = null;
}
}
}
public Models.Stash SelectedStash {
public Models.Stash SelectedStash
{
get => _selectedStash;
set {
if (SetProperty(ref _selectedStash, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedStash, value))
{
if (value == null)
{
Changes = null;
} else {
Task.Run(() => {
}
else
{
Task.Run(() =>
{
var changes = new Commands.QueryStashChanges(_repo.FullPath, value.SHA).Result();
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
Changes = changes;
});
});
@ -36,38 +52,50 @@ namespace SourceGit.ViewModels {
}
}
public List<Models.Change> Changes {
public List<Models.Change> Changes
{
get => _changes;
private set {
if (SetProperty(ref _changes, value)) {
private set
{
if (SetProperty(ref _changes, value))
{
SelectedChange = null;
}
}
}
public Models.Change SelectedChange {
public Models.Change SelectedChange
{
get => _selectedChange;
set {
if (SetProperty(ref _selectedChange, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedChange, value))
{
if (value == null)
{
DiffContext = null;
} else {
}
else
{
DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption($"{_selectedStash.SHA}^", _selectedStash.SHA, value));
}
}
}
}
public DiffContext DiffContext {
public DiffContext DiffContext
{
get => _diffContext;
private set => SetProperty(ref _diffContext, value);
}
public StashesPage(Repository repo) {
public StashesPage(Repository repo)
{
_repo = repo;
}
public void Cleanup() {
public void Cleanup()
{
_repo = null;
if (_stashes != null) _stashes.Clear();
_selectedStash = null;
@ -76,30 +104,40 @@ namespace SourceGit.ViewModels {
_diffContext = null;
}
public void Apply(object param) {
if (param is Models.Stash stash) {
Task.Run(() => {
public void Apply(object param)
{
if (param is Models.Stash stash)
{
Task.Run(() =>
{
new Commands.Stash(_repo.FullPath).Apply(stash.Name);
});
}
}
public void Pop(object param) {
if (param is Models.Stash stash) {
Task.Run(() => {
public void Pop(object param)
{
if (param is Models.Stash stash)
{
Task.Run(() =>
{
new Commands.Stash(_repo.FullPath).Pop(stash.Name);
});
}
}
public void Drop(object param) {
if (param is Models.Stash stash && PopupHost.CanCreatePopup()) {
public void Drop(object param)
{
if (param is Models.Stash stash && PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new DropStash(_repo.FullPath, stash));
}
}
}
public void Clear() {
if (PopupHost.CanCreatePopup()) {
public void Clear()
{
if (PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new ClearStashes(_repo));
}
}
@ -111,4 +149,4 @@ namespace SourceGit.ViewModels {
private Models.Change _selectedChange = null;
private DiffContext _diffContext = null;
}
}
}

View file

@ -1,32 +1,43 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class Statistics : ObservableObject {
public bool IsLoading {
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Statistics : ObservableObject
{
public bool IsLoading
{
get => _isLoading;
private set => SetProperty(ref _isLoading, value);
}
public int SelectedIndex {
public int SelectedIndex
{
get => _selectedIndex;
set {
set
{
if (SetProperty(ref _selectedIndex, value)) RefreshReport();
}
}
public Models.StatisticsReport SelectedReport {
public Models.StatisticsReport SelectedReport
{
get => _selectedReport;
private set => SetProperty(ref _selectedReport, value);
}
public Statistics(string repo) {
public Statistics(string repo)
{
_repo = repo;
Task.Run(() => {
Task.Run(() =>
{
var result = new Commands.Statistics(_repo).Result();
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
_data = result;
RefreshReport();
IsLoading = false;
@ -34,20 +45,22 @@ namespace SourceGit.ViewModels {
});
}
private void RefreshReport() {
private void RefreshReport()
{
if (_data == null) return;
switch (_selectedIndex) {
case 0: SelectedReport = _data.Year; break;
case 1: SelectedReport = _data.Month; break;
default: SelectedReport = _data.Week; break;
switch (_selectedIndex)
{
case 0: SelectedReport = _data.Year; break;
case 1: SelectedReport = _data.Month; break;
default: SelectedReport = _data.Week; break;
}
}
private string _repo = string.Empty;
private readonly string _repo = string.Empty;
private bool _isLoading = true;
private Models.Statistics _data = null;
private Models.StatisticsReport _selectedReport = null;
private int _selectedIndex = 0;
}
}
}

View file

@ -1,44 +1,54 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
using System.Collections.Generic;
namespace SourceGit.ViewModels {
public class TwoSideTextDiff : ObservableObject {
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class TwoSideTextDiff : ObservableObject
{
public string File { get; set; } = string.Empty;
public List<Models.TextDiffLine> Old { get; set; } = new List<Models.TextDiffLine>();
public List<Models.TextDiffLine> New { get; set; } = new List<Models.TextDiffLine>();
public int MaxLineNumber = 0;
public TwoSideTextDiff(Models.TextDiff diff) {
public TwoSideTextDiff(Models.TextDiff diff)
{
File = diff.File;
MaxLineNumber = diff.MaxLineNumber;
foreach (var line in diff.Lines) {
switch (line.Type) {
case Models.TextDiffLineType.Added:
New.Add(line);
break;
case Models.TextDiffLineType.Deleted:
Old.Add(line);
break;
default:
FillEmptyLines();
Old.Add(line);
New.Add(line);
break;
foreach (var line in diff.Lines)
{
switch (line.Type)
{
case Models.TextDiffLineType.Added:
New.Add(line);
break;
case Models.TextDiffLineType.Deleted:
Old.Add(line);
break;
default:
FillEmptyLines();
Old.Add(line);
New.Add(line);
break;
}
}
FillEmptyLines();
}
private void FillEmptyLines() {
if (Old.Count < New.Count) {
private void FillEmptyLines()
{
if (Old.Count < New.Count)
{
int diff = New.Count - Old.Count;
for (int i = 0; i < diff; i++) Old.Add(new Models.TextDiffLine());
} else if (Old.Count > New.Count) {
}
else if (Old.Count > New.Count)
{
int diff = Old.Count - New.Count;
for (int i = 0; i < diff; i++) New.Add(new Models.TextDiffLine());
}
}
}
}
}

View file

@ -1,103 +1,138 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
namespace SourceGit.ViewModels {
public class Welcome : ObservableObject {
public bool IsClearSearchVisible {
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class Welcome : ObservableObject
{
public bool IsClearSearchVisible
{
get => !string.IsNullOrEmpty(_searchFilter);
}
public AvaloniaList<RepositoryNode> RepositoryNodes {
public AvaloniaList<RepositoryNode> RepositoryNodes
{
get => Preference.Instance.RepositoryNodes;
}
public string SearchFilter {
public string SearchFilter
{
get => _searchFilter;
set {
if (SetProperty(ref _searchFilter, value)) {
set
{
if (SetProperty(ref _searchFilter, value))
{
Referesh();
OnPropertyChanged(nameof(IsClearSearchVisible));
}
}
}
public void InitRepository(string path) {
if (!Preference.Instance.IsGitConfigured) {
public void InitRepository(string path)
{
if (!Preference.Instance.IsGitConfigured)
{
App.RaiseException(PopupHost.Active.GetId(), App.Text("NotConfigured"));
return;
}
if (PopupHost.CanCreatePopup()) {
if (PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new Init(path));
}
}
public void Clone(object param) {
public void Clone(object param)
{
var launcher = param as Launcher;
var page = launcher.ActivePage;
if (!Preference.Instance.IsGitConfigured) {
if (!Preference.Instance.IsGitConfigured)
{
App.RaiseException(page.GetId(), App.Text("NotConfigured"));
return;
}
if (PopupHost.CanCreatePopup()) {
if (PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new Clone(launcher, page));
}
}
public void OpenTerminal() {
if (!Preference.Instance.IsGitConfigured) {
public void OpenTerminal()
{
if (!Preference.Instance.IsGitConfigured)
{
App.RaiseException(PopupHost.Active.GetId(), App.Text("NotConfigured"));
} else {
}
else
{
Native.OS.OpenTerminal(null);
}
}
}
public void ClearSearchFilter() {
public void ClearSearchFilter()
{
SearchFilter = string.Empty;
}
public void AddFolder() {
public void AddFolder()
{
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateGroup(null));
}
public void MoveNode(RepositoryNode from, RepositoryNode to) {
public void MoveNode(RepositoryNode from, RepositoryNode to)
{
Preference.MoveNode(from, to);
}
private void Referesh() {
if (string.IsNullOrWhiteSpace(_searchFilter)) {
private void Referesh()
{
if (string.IsNullOrWhiteSpace(_searchFilter))
{
foreach (var node in RepositoryNodes) ResetVisibility(node);
} else {
}
else
{
foreach (var node in RepositoryNodes) SetVisibilityBySearch(node);
}
}
private void ResetVisibility(RepositoryNode node) {
private void ResetVisibility(RepositoryNode node)
{
node.IsVisible = true;
foreach (var subNode in node.SubNodes) ResetVisibility(subNode);
}
private void SetVisibilityBySearch(RepositoryNode node) {
if (!node.IsRepository) {
if (node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) {
private void SetVisibilityBySearch(RepositoryNode node)
{
if (!node.IsRepository)
{
if (node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))
{
node.IsVisible = true;
foreach (var subNode in node.SubNodes) ResetVisibility(subNode);
} else {
}
else
{
bool hasVisibleSubNode = false;
foreach (var subNode in node.SubNodes) {
foreach (var subNode in node.SubNodes)
{
SetVisibilityBySearch(subNode);
hasVisibleSubNode |= subNode.IsVisible;
}
node.IsVisible = hasVisibleSubNode;
}
} else {
}
else
{
node.IsVisible = node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase);
}
}
private string _searchFilter = string.Empty;
}
}
}

View file

@ -1,57 +1,72 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace SourceGit.ViewModels {
public class ConflictContext {
using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class ConflictContext
{
public Models.Change Change { get; set; }
}
public class WorkingCopy : ObservableObject {
public bool IsStaging {
public class WorkingCopy : ObservableObject
{
public bool IsStaging
{
get => _isStaging;
private set => SetProperty(ref _isStaging, value);
}
public bool IsUnstaging {
public bool IsUnstaging
{
get => _isUnstaging;
private set => SetProperty(ref _isUnstaging, value);
}
public bool IsCommitting {
get => _isCommitting;
public bool IsCommitting
{
get => _isCommitting;
private set => SetProperty(ref _isCommitting, value);
}
public bool UseAmend {
public bool UseAmend
{
get => _useAmend;
set => SetProperty(ref _useAmend, value);
}
public List<Models.Change> Unstaged {
public List<Models.Change> Unstaged
{
get => _unstaged;
private set => SetProperty(ref _unstaged, value);
}
public List<Models.Change> Staged {
public List<Models.Change> Staged
{
get => _staged;
private set => SetProperty(ref _staged, value);
}
public int Count {
public int Count
{
get => _count;
}
public Models.Change SelectedUnstagedChange {
public Models.Change SelectedUnstagedChange
{
get => _selectedUnstagedChange;
set {
if (SetProperty(ref _selectedUnstagedChange, value) && value != null) {
set
{
if (SetProperty(ref _selectedUnstagedChange, value) && value != null)
{
SelectedStagedChange = null;
SelectedStagedTreeNode = null;
SetDetail(value, true);
@ -59,10 +74,13 @@ namespace SourceGit.ViewModels {
}
}
public Models.Change SelectedStagedChange {
public Models.Change SelectedStagedChange
{
get => _selectedStagedChange;
set {
if (SetProperty(ref _selectedStagedChange, value) && value != null) {
set
{
if (SetProperty(ref _selectedStagedChange, value) && value != null)
{
SelectedUnstagedChange = null;
SelectedUnstagedTreeNode = null;
SetDetail(value, false);
@ -70,28 +88,37 @@ namespace SourceGit.ViewModels {
}
}
public List<FileTreeNode> UnstagedTree {
get => _unstagedTree;
public List<FileTreeNode> UnstagedTree
{
get => _unstagedTree;
private set => SetProperty(ref _unstagedTree, value);
}
public List<FileTreeNode> StagedTree {
public List<FileTreeNode> StagedTree
{
get => _stagedTree;
private set => SetProperty(ref _stagedTree, value);
}
public FileTreeNode SelectedUnstagedTreeNode {
public FileTreeNode SelectedUnstagedTreeNode
{
get => _selectedUnstagedTreeNode;
set {
if (SetProperty(ref _selectedUnstagedTreeNode, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedUnstagedTreeNode, value))
{
if (value == null)
{
SelectedUnstagedChange = null;
} else {
}
else
{
SelectedUnstagedChange = value.Backend as Models.Change;
SelectedStagedTreeNode = null;
SelectedStagedChange = null;
if (value.IsFolder) {
if (value.IsFolder)
{
SetDetail(null, true);
}
}
@ -99,18 +126,25 @@ namespace SourceGit.ViewModels {
}
}
public FileTreeNode SelectedStagedTreeNode {
public FileTreeNode SelectedStagedTreeNode
{
get => _selectedStagedTreeNode;
set {
if (SetProperty(ref _selectedStagedTreeNode, value)) {
if (value == null) {
set
{
if (SetProperty(ref _selectedStagedTreeNode, value))
{
if (value == null)
{
SelectedStagedChange = null;
} else {
}
else
{
SelectedStagedChange = value.Backend as Models.Change;
SelectedUnstagedTreeNode = null;
SelectedUnstagedChange = null;
if (value.IsFolder) {
if (value.IsFolder)
{
SetDetail(null, false);
}
}
@ -118,21 +152,25 @@ namespace SourceGit.ViewModels {
}
}
public object DetailContext {
public object DetailContext
{
get => _detailContext;
private set => SetProperty(ref _detailContext, value);
}
public string CommitMessage {
public string CommitMessage
{
get => _commitMessage;
set => SetProperty(ref _commitMessage, value);
}
public WorkingCopy(Repository repo) {
public WorkingCopy(Repository repo)
{
_repo = repo;
}
public void Cleanup() {
public void Cleanup()
{
_repo = null;
if (_unstaged != null) _unstaged.Clear();
if (_staged != null) _staged.Clear();
@ -146,36 +184,45 @@ namespace SourceGit.ViewModels {
_commitMessage = string.Empty;
}
public bool SetData(List<Models.Change> changes) {
public bool SetData(List<Models.Change> changes)
{
var unstaged = new List<Models.Change>();
var staged = new List<Models.Change>();
var viewFile = string.Empty;
var lastSelectedIsUnstaged = false;
if (_selectedUnstagedChange != null) {
if (_selectedUnstagedChange != null)
{
viewFile = _selectedUnstagedChange.Path;
lastSelectedIsUnstaged = true;
} else if (_selectedStagedChange != null) {
}
else if (_selectedStagedChange != null)
{
viewFile = _selectedStagedChange.Path;
}
var viewChange = null as Models.Change;
var hasConflict = false;
foreach (var c in changes) {
foreach (var c in changes)
{
if (c.Index == Models.ChangeState.Modified
|| c.Index == Models.ChangeState.Added
|| c.Index == Models.ChangeState.Deleted
|| c.Index == Models.ChangeState.Renamed) {
|| c.Index == Models.ChangeState.Renamed)
{
staged.Add(c);
if (!lastSelectedIsUnstaged && c.Path == viewFile) {
if (!lastSelectedIsUnstaged && c.Path == viewFile)
{
viewChange = c;
}
}
if (c.WorkTree != Models.ChangeState.None) {
if (c.WorkTree != Models.ChangeState.None)
{
unstaged.Add(c);
hasConflict |= c.IsConflit;
if (lastSelectedIsUnstaged && c.Path == viewFile) {
if (lastSelectedIsUnstaged && c.Path == viewFile)
{
viewChange = c;
}
}
@ -185,7 +232,8 @@ namespace SourceGit.ViewModels {
var unstagedTree = FileTreeNode.Build(unstaged);
var stagedTree = FileTreeNode.Build(staged);
Dispatcher.UIThread.Invoke(() => {
Dispatcher.UIThread.Invoke(() =>
{
_isLoadingData = true;
Unstaged = unstaged;
Staged = staged;
@ -194,20 +242,26 @@ namespace SourceGit.ViewModels {
_isLoadingData = false;
// Restore last selection states.
if (viewChange != null) {
if (viewChange != null)
{
var scrollOffset = Vector.Zero;
if (_detailContext is DiffContext old) scrollOffset = old.SyncScrollOffset;
if (lastSelectedIsUnstaged) {
if (lastSelectedIsUnstaged)
{
SelectedUnstagedChange = viewChange;
SelectedUnstagedTreeNode = FileTreeNode.SelectByPath(_unstagedTree, viewFile);
} else {
}
else
{
SelectedStagedChange = viewChange;
SelectedStagedTreeNode = FileTreeNode.SelectByPath(_stagedTree, viewFile);
}
if (_detailContext is DiffContext cur) cur.SyncScrollOffset = scrollOffset;
} else {
}
else
{
SelectedUnstagedChange = null;
SelectedUnstagedTreeNode = null;
SelectedStagedChange = null;
@ -219,28 +273,39 @@ namespace SourceGit.ViewModels {
return hasConflict;
}
public void SetDetail(Models.Change change, bool isUnstaged) {
public void SetDetail(Models.Change change, bool isUnstaged)
{
if (_isLoadingData) return;
if (change == null) {
if (change == null)
{
DetailContext = null;
} else if (change.IsConflit) {
}
else if (change.IsConflit)
{
DetailContext = new ConflictContext() { Change = change };
} else {
}
else
{
DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged));
}
}
public async void StageChanges(List<Models.Change> changes) {
public async void StageChanges(List<Models.Change> changes)
{
if (_unstaged.Count == 0 || changes.Count == 0) return;
SetDetail(null, true);
IsStaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _unstaged.Count) {
if (changes.Count == _unstaged.Count)
{
await Task.Run(() => new Commands.Add(_repo.FullPath).Exec());
} else {
for (int i = 0; i < changes.Count; i += 10) {
}
else
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var step = changes.GetRange(i, count);
await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec());
@ -251,16 +316,21 @@ namespace SourceGit.ViewModels {
IsStaging = false;
}
public async void UnstageChanges(List<Models.Change> changes) {
public async void UnstageChanges(List<Models.Change> changes)
{
if (_staged.Count == 0 || changes.Count == 0) return;
SetDetail(null, false);
IsUnstaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _staged.Count) {
if (changes.Count == _staged.Count)
{
await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
} else {
for (int i = 0; i < changes.Count; i += 10) {
}
else
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var step = changes.GetRange(i, count);
await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec());
@ -271,29 +341,43 @@ namespace SourceGit.ViewModels {
IsUnstaging = false;
}
public void Discard(List<Models.Change> changes, bool isUnstaged) {
if (PopupHost.CanCreatePopup()) {
if (isUnstaged) {
if (changes.Count == _unstaged.Count && _staged.Count == 0) {
public void Discard(List<Models.Change> changes, bool isUnstaged)
{
if (PopupHost.CanCreatePopup())
{
if (isUnstaged)
{
if (changes.Count == _unstaged.Count && _staged.Count == 0)
{
PopupHost.ShowPopup(new Discard(_repo));
} else {
}
else
{
PopupHost.ShowPopup(new Discard(_repo, changes, true));
}
} else {
if (changes.Count == _staged.Count && _unstaged.Count == 0) {
}
else
{
if (changes.Count == _staged.Count && _unstaged.Count == 0)
{
PopupHost.ShowPopup(new Discard(_repo));
} else {
}
else
{
PopupHost.ShowPopup(new Discard(_repo, changes, false));
}
}
}
}
}
public async void UseTheirs() {
if (_detailContext is ConflictContext ctx) {
public async void UseTheirs()
{
if (_detailContext is ConflictContext ctx)
{
_repo.SetWatcherEnabled(false);
var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).File(ctx.Change.Path, true));
if (succ) {
if (succ)
{
await Task.Run(() => new Commands.Add(_repo.FullPath, [ctx.Change]).Exec());
}
_repo.MarkWorkingCopyDirtyManually();
@ -301,11 +385,14 @@ namespace SourceGit.ViewModels {
}
}
public async void UseMine() {
if (_detailContext is ConflictContext ctx) {
public async void UseMine()
{
if (_detailContext is ConflictContext ctx)
{
_repo.SetWatcherEnabled(false);
var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).File(ctx.Change.Path, false));
if (succ) {
if (succ)
{
await Task.Run(() => new Commands.Add(_repo.FullPath, [ctx.Change]).Exec());
}
_repo.MarkWorkingCopyDirtyManually();
@ -313,13 +400,16 @@ namespace SourceGit.ViewModels {
}
}
public async void UseExternalMergeTool() {
if (_detailContext is ConflictContext ctx) {
public async void UseExternalMergeTool()
{
if (_detailContext is ConflictContext ctx)
{
var type = Preference.Instance.ExternalMergeToolType;
var exec = Preference.Instance.ExternalMergeToolPath;
var tool = Models.ExternalMergeTools.Supported.Find(x => x.Type == type);
if (tool == null) {
if (tool == null)
{
App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!");
return;
}
@ -332,18 +422,22 @@ namespace SourceGit.ViewModels {
}
}
public async void DoCommit(bool autoPush) {
if (!PopupHost.CanCreatePopup()) {
public async void DoCommit(bool autoPush)
{
if (!PopupHost.CanCreatePopup())
{
App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!");
return;
}
if (_staged.Count == 0) {
if (_staged.Count == 0)
{
App.RaiseException(_repo.FullPath, "No files added to commit!");
return;
}
if (string.IsNullOrWhiteSpace(_commitMessage)) {
if (string.IsNullOrWhiteSpace(_commitMessage))
{
App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!");
return;
}
@ -354,11 +448,13 @@ namespace SourceGit.ViewModels {
IsCommitting = true;
_repo.SetWatcherEnabled(false);
var succ = await Task.Run(() => new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec());
if (succ) {
if (succ)
{
CommitMessage = string.Empty;
UseAmend = false;
if (autoPush) {
if (autoPush)
{
PopupHost.ShowAndStartPopup(new Push(_repo, null));
}
}
@ -367,11 +463,13 @@ namespace SourceGit.ViewModels {
IsCommitting = false;
}
public ContextMenu CreateContextMenuForUnstagedChanges(List<Models.Change> changes) {
public ContextMenu CreateContextMenuForUnstagedChanges(List<Models.Change> changes)
{
if (changes.Count == 0) return null;
var menu = new ContextMenu();
if (changes.Count == 1) {
if (changes.Count == 1)
{
var change = changes[0];
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
@ -379,16 +477,18 @@ namespace SourceGit.ViewModels {
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Folder.Open");
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Click += (_, e) => {
explore.Click += (_, e) =>
{
Native.OS.OpenInFileManager(path, true);
e.Handled = true;
};
var openWith = new MenuItem();
openWith.Header = App.Text("OpenWith");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.IsEnabled = File.Exists(path);
openWith.Click += (_, e) => {
openWith.Click += (_, e) =>
{
Native.OS.OpenWithDefaultEditor(path);
e.Handled = true;
};
@ -396,7 +496,8 @@ namespace SourceGit.ViewModels {
var stage = new MenuItem();
stage.Header = App.Text("FileCM.Stage");
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) => {
stage.Click += (_, e) =>
{
StageChanges(changes);
e.Handled = true;
};
@ -404,7 +505,8 @@ namespace SourceGit.ViewModels {
var discard = new MenuItem();
discard.Header = App.Text("FileCM.Discard");
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) => {
discard.Click += (_, e) =>
{
Discard(changes, true);
e.Handled = true;
};
@ -412,8 +514,10 @@ namespace SourceGit.ViewModels {
var stash = new MenuItem();
stash.Header = App.Text("FileCM.Stash");
stash.Icon = App.CreateMenuIcon("Icons.Stashes");
stash.Click += (_, e) => {
if (PopupHost.CanCreatePopup()) {
stash.Click += (_, e) =>
{
if (PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
}
e.Handled = true;
@ -422,7 +526,8 @@ namespace SourceGit.ViewModels {
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) => {
patch.Click += async (_, e) =>
{
var topLevel = App.GetTopLevel();
if (topLevel == null) return;
@ -432,7 +537,8 @@ namespace SourceGit.ViewModels {
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null) {
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath));
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@ -443,7 +549,8 @@ namespace SourceGit.ViewModels {
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) => {
history.Click += (_, e) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo.FullPath, change.Path) };
window.Show();
e.Handled = true;
@ -453,7 +560,8 @@ namespace SourceGit.ViewModels {
assumeUnchanged.Header = App.Text("FileCM.AssumeUnchanged");
assumeUnchanged.Icon = App.CreateMenuIcon("Icons.File.Ignore");
assumeUnchanged.IsEnabled = change.WorkTree != Models.ChangeState.Untracked;
assumeUnchanged.Click += (_, e) => {
assumeUnchanged.Click += (_, e) =>
{
new Commands.AssumeUnchanged(_repo.FullPath).Add(change.Path);
e.Handled = true;
};
@ -461,7 +569,8 @@ namespace SourceGit.ViewModels {
var copy = new MenuItem();
copy.Header = App.Text("CopyPath");
copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (_, e) => {
copy.Click += (_, e) =>
{
App.CopyText(change.Path);
e.Handled = true;
};
@ -478,11 +587,14 @@ namespace SourceGit.ViewModels {
menu.Items.Add(assumeUnchanged);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copy);
} else {
}
else
{
var stage = new MenuItem();
stage.Header = App.Text("FileCM.StageMulti", changes.Count);
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) => {
stage.Click += (_, e) =>
{
StageChanges(changes);
e.Handled = true;
};
@ -490,7 +602,8 @@ namespace SourceGit.ViewModels {
var discard = new MenuItem();
discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) => {
discard.Click += (_, e) =>
{
Discard(changes, true);
e.Handled = true;
};
@ -498,8 +611,10 @@ namespace SourceGit.ViewModels {
var stash = new MenuItem();
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
stash.Icon = App.CreateMenuIcon("Icons.Stashes");
stash.Click += (_, e) => {
if (PopupHost.CanCreatePopup()) {
stash.Click += (_, e) =>
{
if (PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
}
e.Handled = true;
@ -508,7 +623,8 @@ namespace SourceGit.ViewModels {
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (o, e) => {
patch.Click += async (o, e) =>
{
var topLevel = App.GetTopLevel();
if (topLevel == null) return;
@ -516,9 +632,10 @@ namespace SourceGit.ViewModels {
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null) {
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath));
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@ -530,16 +647,18 @@ namespace SourceGit.ViewModels {
menu.Items.Add(discard);
menu.Items.Add(stash);
menu.Items.Add(patch);
}
}
return menu;
}
public ContextMenu CreateContextMenuForStagedChanges(List<Models.Change> changes) {
public ContextMenu CreateContextMenuForStagedChanges(List<Models.Change> changes)
{
if (changes.Count == 0) return null;
var menu = new ContextMenu();
if (changes.Count == 1) {
if (changes.Count == 1)
{
var change = changes[0];
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
@ -547,7 +666,8 @@ namespace SourceGit.ViewModels {
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Folder.Open");
explore.Click += (o, e) => {
explore.Click += (o, e) =>
{
Native.OS.OpenInFileManager(path, true);
e.Handled = true;
};
@ -556,7 +676,8 @@ namespace SourceGit.ViewModels {
openWith.Header = App.Text("OpenWith");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.IsEnabled = File.Exists(path);
openWith.Click += (_, e) => {
openWith.Click += (_, e) =>
{
Native.OS.OpenWithDefaultEditor(path);
e.Handled = true;
};
@ -564,7 +685,8 @@ namespace SourceGit.ViewModels {
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.Unstage");
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (o, e) => {
unstage.Click += (o, e) =>
{
UnstageChanges(changes);
e.Handled = true;
};
@ -572,7 +694,8 @@ namespace SourceGit.ViewModels {
var discard = new MenuItem();
discard.Header = App.Text("FileCM.Discard");
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) => {
discard.Click += (_, e) =>
{
Discard(changes, false);
e.Handled = true;
};
@ -580,8 +703,10 @@ namespace SourceGit.ViewModels {
var stash = new MenuItem();
stash.Header = App.Text("FileCM.Stash");
stash.Icon = App.CreateMenuIcon("Icons.Stashes");
stash.Click += (_, e) => {
if (PopupHost.CanCreatePopup()) {
stash.Click += (_, e) =>
{
if (PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
}
e.Handled = true;
@ -590,7 +715,8 @@ namespace SourceGit.ViewModels {
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (o, e) => {
patch.Click += async (o, e) =>
{
var topLevel = App.GetTopLevel();
if (topLevel == null) return;
@ -600,7 +726,8 @@ namespace SourceGit.ViewModels {
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null) {
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath));
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@ -611,7 +738,8 @@ namespace SourceGit.ViewModels {
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Click += (o, e) => {
copyPath.Click += (o, e) =>
{
App.CopyText(change.Path);
e.Handled = true;
};
@ -625,11 +753,14 @@ namespace SourceGit.ViewModels {
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copyPath);
} else {
}
else
{
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.UnstageMulti", changes.Count);
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (o, e) => {
unstage.Click += (o, e) =>
{
UnstageChanges(changes);
e.Handled = true;
};
@ -637,7 +768,8 @@ namespace SourceGit.ViewModels {
var discard = new MenuItem();
discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) => {
discard.Click += (_, e) =>
{
Discard(changes, false);
e.Handled = true;
};
@ -645,8 +777,10 @@ namespace SourceGit.ViewModels {
var stash = new MenuItem();
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
stash.Icon = App.CreateMenuIcon("Icons.Stashes");
stash.Click += (_, e) => {
if (PopupHost.CanCreatePopup()) {
stash.Click += (_, e) =>
{
if (PopupHost.CanCreatePopup())
{
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
}
e.Handled = true;
@ -655,7 +789,8 @@ namespace SourceGit.ViewModels {
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) => {
patch.Click += async (_, e) =>
{
var topLevel = App.GetTopLevel();
if (topLevel == null) return;
@ -665,7 +800,8 @@ namespace SourceGit.ViewModels {
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options);
if (storageFile != null) {
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath));
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
@ -682,9 +818,11 @@ namespace SourceGit.ViewModels {
return menu;
}
public ContextMenu CreateContextMenuForCommitMessages() {
public ContextMenu CreateContextMenuForCommitMessages()
{
var menu = new ContextMenu();
if (_repo.CommitMessages.Count == 0) {
if (_repo.CommitMessages.Count == 0)
{
var empty = new MenuItem();
empty.Header = App.Text("WorkingCopy.NoCommitHistories");
empty.IsEnabled = false;
@ -698,12 +836,14 @@ namespace SourceGit.ViewModels {
menu.Items.Add(tip);
menu.Items.Add(new MenuItem() { Header = "-" });
foreach (var message in _repo.CommitMessages) {
foreach (var message in _repo.CommitMessages)
{
var dump = message;
var item = new MenuItem();
item.Header = dump;
item.Click += (o, e) => {
item.Click += (o, e) =>
{
CommitMessage = dump;
e.Handled = true;
};
@ -714,16 +854,21 @@ namespace SourceGit.ViewModels {
return menu;
}
private void PushCommitMessage() {
private void PushCommitMessage()
{
var existIdx = _repo.CommitMessages.IndexOf(CommitMessage);
if (existIdx == 0) {
if (existIdx == 0)
{
return;
} else if (existIdx > 0) {
}
else if (existIdx > 0)
{
_repo.CommitMessages.Move(existIdx, 0);
return;
}
if (_repo.CommitMessages.Count > 9) {
if (_repo.CommitMessages.Count > 9)
{
_repo.CommitMessages.RemoveRange(9, _repo.CommitMessages.Count - 9);
}
@ -748,4 +893,4 @@ namespace SourceGit.ViewModels {
private object _detailContext = null;
private string _commitMessage = string.Empty;
}
}
}