mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-21 20:24:59 +00:00
refactor<*>: rewrite all with AvaloniaUI
This commit is contained in:
parent
0136904612
commit
2a62596999
521 changed files with 19780 additions and 23244 deletions
82
src/ViewModels/AddRemote.cs
Normal file
82
src/ViewModels/AddRemote.cs
Normal file
|
@ -0,0 +1,82 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value, true);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Remote URL is required!!!")]
|
||||
[CustomValidation(typeof(AddRemote), nameof(ValidateRemoteURL))]
|
||||
public string Url {
|
||||
get => _url;
|
||||
set {
|
||||
if (SetProperty(ref _url, value, true)) UseSSH = Models.Remote.IsSSH(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseSSH {
|
||||
get => _useSSH;
|
||||
set => SetProperty(ref _useSSH, value);
|
||||
}
|
||||
|
||||
public string SSHKey {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
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) {
|
||||
var exists = add._repo.Remotes.Find(x => x.Name == name);
|
||||
if (exists != null) return new ValidationResult("A remote with given name already exists!!!");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
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);
|
||||
if (exists != null) return new ValidationResult("A remote with the same url already exists!!!");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription("Adding remote ...");
|
||||
var succ = new Commands.Remote(_repo.FullPath).Add(_name, _url);
|
||||
if (succ) {
|
||||
SetProgressDescription("Fetching from added remote ...");
|
||||
new Commands.Fetch(_repo.FullPath, _name, true, SetProgressDescription).Exec();
|
||||
|
||||
if (_useSSH) {
|
||||
SetProgressDescription("Post processing ...");
|
||||
new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", SSHKey);
|
||||
}
|
||||
}
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _name = string.Empty;
|
||||
private string _url = string.Empty;
|
||||
private bool _useSSH = false;
|
||||
}
|
||||
}
|
61
src/ViewModels/AddSubmodule.cs
Normal file
61
src/ViewModels/AddSubmodule.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class AddSubmodule : Popup {
|
||||
[Required(ErrorMessage = "Url is required!!!")]
|
||||
[CustomValidation(typeof(AddSubmodule), nameof(ValidateURL))]
|
||||
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 {
|
||||
get => _relativePath;
|
||||
set => SetProperty(ref _relativePath, value, true);
|
||||
}
|
||||
|
||||
public bool Recursive {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public AddSubmodule(Repository repo) {
|
||||
_repo = repo;
|
||||
View = new Views.AddSubmodule() { DataContext = this };
|
||||
}
|
||||
|
||||
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)) {
|
||||
return new ValidationResult("Give path is exists already!");
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(path)) {
|
||||
return new ValidationResult("Path must be relative to this repository!");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
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 string _url = string.Empty;
|
||||
private string _relativePath = string.Empty;
|
||||
}
|
||||
}
|
65
src/ViewModels/Apply.cs
Normal file
65
src/ViewModels/Apply.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Apply : Popup {
|
||||
[Required(ErrorMessage = "Patch file is required!!!")]
|
||||
[CustomValidation(typeof(Apply), nameof(ValidatePatchFile))]
|
||||
public string PatchFile {
|
||||
get => _patchFile;
|
||||
set => SetProperty(ref _patchFile, value, true);
|
||||
}
|
||||
|
||||
public bool IgnoreWhiteSpace {
|
||||
get => _ignoreWhiteSpace;
|
||||
set => SetProperty(ref _ignoreWhiteSpace, value);
|
||||
}
|
||||
|
||||
public List<Models.ApplyWhiteSpaceMode> WhiteSpaceModes {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Models.ApplyWhiteSpaceMode SelectedWhiteSpaceMode {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Apply(Repository repo) {
|
||||
_repo = repo;
|
||||
|
||||
WhiteSpaceModes = new List<Models.ApplyWhiteSpaceMode> {
|
||||
new Models.ApplyWhiteSpaceMode("Apply.NoWarn", "Apply.NoWarn.Desc", "nowarn"),
|
||||
new Models.ApplyWhiteSpaceMode("Apply.Warn", "Apply.Warn.Desc", "warn"),
|
||||
new Models.ApplyWhiteSpaceMode("Apply.Error", "Apply.Error.Desc", "error"),
|
||||
new Models.ApplyWhiteSpaceMode("Apply.ErrorAll", "Apply.ErrorAll.Desc", "error-all")
|
||||
};
|
||||
SelectedWhiteSpaceMode = WhiteSpaceModes[0];
|
||||
|
||||
View = new Views.Apply() { DataContext = this };
|
||||
}
|
||||
|
||||
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() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
var succ = new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _patchFile = string.Empty;
|
||||
private bool _ignoreWhiteSpace = true;
|
||||
}
|
||||
}
|
56
src/ViewModels/Archive.cs
Normal file
56
src/ViewModels/Archive.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Archive : Popup {
|
||||
|
||||
[Required(ErrorMessage = "Output file name is required")]
|
||||
public string SaveFile {
|
||||
get => _saveFile;
|
||||
set => SetProperty(ref _saveFile, value, true);
|
||||
}
|
||||
|
||||
public object BasedOn {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Archive(Repository repo, Models.Branch branch) {
|
||||
_repo = repo;
|
||||
_revision = branch.Head;
|
||||
_saveFile = $"archive-{Path.GetFileNameWithoutExtension(branch.Name)}.zip";
|
||||
BasedOn = branch;
|
||||
View = new Views.Archive() { DataContext = this };
|
||||
}
|
||||
|
||||
public Archive(Repository repo, Models.Commit commit) {
|
||||
_repo = repo;
|
||||
_revision = commit.SHA;
|
||||
_saveFile = $"archive-{commit.SHA.Substring(0,10)}.zip";
|
||||
BasedOn = commit;
|
||||
View = new Views.Archive() { DataContext = this };
|
||||
}
|
||||
|
||||
public Archive(Repository repo, Models.Tag tag) {
|
||||
_repo = repo;
|
||||
_revision = tag.SHA;
|
||||
_saveFile = $"archive-{tag.Name}.zip";
|
||||
BasedOn = tag;
|
||||
View = new Views.Archive() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
var succ = new Commands.Archive(_repo.FullPath, _revision, _saveFile, SetProgressDescription).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _saveFile = string.Empty;
|
||||
private string _revision = string.Empty;
|
||||
}
|
||||
}
|
30
src/ViewModels/AssumeUnchangedManager.cs
Normal file
30
src/ViewModels/AssumeUnchangedManager.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using Avalonia.Collections;
|
||||
using Avalonia.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class AssumeUnchangedManager {
|
||||
public AvaloniaList<string> Files { get; private set; }
|
||||
|
||||
public AssumeUnchangedManager(string repo) {
|
||||
_repo = repo;
|
||||
Files = new AvaloniaList<string>();
|
||||
|
||||
Task.Run(() => {
|
||||
var collect = new Commands.AssumeUnchanged(_repo).View();
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
Files.AddRange(collect);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void Remove(object param) {
|
||||
if (param is string file) {
|
||||
new Commands.AssumeUnchanged(_repo).Remove(file);
|
||||
Files.Remove(file);
|
||||
}
|
||||
}
|
||||
|
||||
private string _repo;
|
||||
}
|
||||
}
|
48
src/ViewModels/Blame.cs
Normal file
48
src/ViewModels/Blame.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Blame : ObservableObject {
|
||||
public string Title {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string SelectedSHA {
|
||||
get => _selectedSHA;
|
||||
private set => SetProperty(ref _selectedSHA, value);
|
||||
}
|
||||
|
||||
public bool IsBinary {
|
||||
get => _data != null && _data.IsBinary;
|
||||
}
|
||||
|
||||
public Models.BlameData Data {
|
||||
get => _data;
|
||||
private set => SetProperty(ref _data, value);
|
||||
}
|
||||
|
||||
public Blame(string repo, string file, string revision) {
|
||||
_repo = repo;
|
||||
|
||||
Title = $"{file}@{revision.Substring(0, 10)}";
|
||||
Task.Run(() => {
|
||||
var result = new Commands.Blame(repo, file, revision).Result();
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
Data = result;
|
||||
OnPropertyChanged(nameof(IsBinary));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void NavigateToCommit(string commitSHA) {
|
||||
var repo = Preference.FindRepository(_repo);
|
||||
if (repo != null) repo.NavigateToCommit(commitSHA);
|
||||
}
|
||||
|
||||
private string _repo = string.Empty;
|
||||
private string _selectedSHA = string.Empty;
|
||||
private Models.BlameData _data = null;
|
||||
}
|
||||
}
|
28
src/ViewModels/Checkout.cs
Normal file
28
src/ViewModels/Checkout.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Checkout : Popup {
|
||||
public string Branch {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Checkout(Repository repo, string branch) {
|
||||
_repo = repo;
|
||||
Branch = branch;
|
||||
View = new Views.Checkout() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Checkout '{Branch}' ...");
|
||||
var succ = new Commands.Checkout(_repo.FullPath).Branch(Branch, SetProgressDescription);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo;
|
||||
}
|
||||
}
|
34
src/ViewModels/CherryPick.cs
Normal file
34
src/ViewModels/CherryPick.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class CherryPick : Popup {
|
||||
public Models.Commit Target {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool AutoCommit {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public CherryPick(Repository repo, Models.Commit target) {
|
||||
_repo = repo;
|
||||
Target = target;
|
||||
AutoCommit = true;
|
||||
View = new Views.CherryPick() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Cherry-Pick commit '{Target.SHA}' ...");
|
||||
var succ = new Commands.CherryPick(_repo.FullPath, Target.SHA, !AutoCommit).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
30
src/ViewModels/Cleanup.cs
Normal file
30
src/ViewModels/Cleanup.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Cleanup : Popup {
|
||||
public Cleanup(Repository repo) {
|
||||
_repo = repo;
|
||||
View = new Views.Cleanup() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
ProgressDescription = "Cleanup (GC & prune) ...";
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription("Run GC ...");
|
||||
new Commands.GC(_repo.FullPath, SetProgressDescription).Exec();
|
||||
|
||||
var lfs = new Commands.LFS(_repo.FullPath);
|
||||
if (lfs.IsEnabled()) {
|
||||
SetProgressDescription("Run LFS prune ...");
|
||||
lfs.Prune(SetProgressDescription);
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
21
src/ViewModels/ClearStashes.cs
Normal file
21
src/ViewModels/ClearStashes.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class ClearStashes : Popup {
|
||||
public ClearStashes(Repository repo) {
|
||||
_repo = repo;
|
||||
View = new Views.ClearStashes() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
new Commands.Stash(_repo.FullPath).Clear();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
111
src/ViewModels/Clone.cs
Normal file
111
src/ViewModels/Clone.cs
Normal file
|
@ -0,0 +1,111 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Clone : Popup {
|
||||
[Required(ErrorMessage = "Remote URL is required")]
|
||||
[CustomValidation(typeof(Clone), nameof(ValidateRemote))]
|
||||
public string Remote {
|
||||
get => _remote;
|
||||
set {
|
||||
if (SetProperty(ref _remote, value, true)) UseSSH = Models.Remote.IsSSH(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseSSH {
|
||||
get => _useSSH;
|
||||
set => SetProperty(ref _useSSH, value);
|
||||
}
|
||||
|
||||
public string SSHKey {
|
||||
get => _sshKey;
|
||||
set => SetProperty(ref _sshKey, value);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Parent folder is required")]
|
||||
[CustomValidation(typeof(Clone), nameof(ValidateParentFolder))]
|
||||
public string ParentFolder {
|
||||
get => _parentFolder;
|
||||
set => SetProperty(ref _parentFolder, value, true);
|
||||
}
|
||||
|
||||
public string Local {
|
||||
get => _local;
|
||||
set => SetProperty(ref _local, value);
|
||||
}
|
||||
|
||||
public string ExtraArgs {
|
||||
get => _extraArgs;
|
||||
set => SetProperty(ref _extraArgs, value);
|
||||
}
|
||||
|
||||
public Clone(LauncherPage page) {
|
||||
View = new Views.Clone() { DataContext = this };
|
||||
_page = page;
|
||||
}
|
||||
|
||||
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 _) {
|
||||
if (!Directory.Exists(folder)) return new ValidationResult("Given path can NOT be found");
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
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)) {
|
||||
path = Path.GetFullPath(Path.Combine(path, _local));
|
||||
} 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(() => {
|
||||
App.RaiseException(HostPageId, $"Folder '{path}' can NOT be found");
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_useSSH && !string.IsNullOrEmpty(_sshKey)) {
|
||||
var config = new Commands.Config(path);
|
||||
config.Set("remote.origin.sshkey", _sshKey);
|
||||
}
|
||||
|
||||
CallUIThread(() => {
|
||||
var repo = Preference.AddRepository(path, Path.Combine(path, ".git"));
|
||||
var node = new RepositoryNode() {
|
||||
Id = path,
|
||||
Name = Path.GetFileName(path),
|
||||
Bookmark = 0,
|
||||
IsRepository = true,
|
||||
};
|
||||
Preference.AddNode(node);
|
||||
|
||||
_page.View = new Views.Repository() { DataContext = repo };
|
||||
_page.Node = node;
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private LauncherPage _page = null;
|
||||
private string _remote = string.Empty;
|
||||
private bool _useSSH = false;
|
||||
private string _sshKey = string.Empty;
|
||||
private string _parentFolder = Preference.Instance.GitDefaultCloneDir;
|
||||
private string _local = string.Empty;
|
||||
private string _extraArgs = string.Empty;
|
||||
}
|
||||
}
|
408
src/ViewModels/CommitDetail.cs
Normal file
408
src/ViewModels/CommitDetail.cs
Normal file
|
@ -0,0 +1,408 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class CommitDetail : ObservableObject {
|
||||
public DiffContext DiffContext {
|
||||
get => _diffContext;
|
||||
private set => SetProperty(ref _diffContext, value);
|
||||
}
|
||||
|
||||
public int ActivePageIndex {
|
||||
get => _activePageIndex;
|
||||
set => SetProperty(ref _activePageIndex, value);
|
||||
}
|
||||
|
||||
public Models.Commit Commit {
|
||||
get => _commit;
|
||||
set {
|
||||
if (SetProperty(ref _commit, value)) Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Models.Change> Changes {
|
||||
get => _changes;
|
||||
set => SetProperty(ref _changes, value);
|
||||
}
|
||||
|
||||
public List<Models.Change> VisibleChanges {
|
||||
get => _visibleChanges;
|
||||
set => SetProperty(ref _visibleChanges, value);
|
||||
}
|
||||
|
||||
public List<FileTreeNode> ChangeTree {
|
||||
get => _changeTree;
|
||||
set => SetProperty(ref _changeTree, value);
|
||||
}
|
||||
|
||||
public Models.Change SelectedChange {
|
||||
get => _selectedChange;
|
||||
set {
|
||||
if (SetProperty(ref _selectedChange, value)) {
|
||||
if (value == null) {
|
||||
SelectedChangeNode = null;
|
||||
DiffContext = null;
|
||||
} else {
|
||||
SelectedChangeNode = FileTreeNode.SelectByPath(_changeTree, value.Path);
|
||||
DiffContext = new DiffContext(_repo, new Models.DiffOption(_commit, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FileTreeNode SelectedChangeNode {
|
||||
get => _selectedChangeNode;
|
||||
set {
|
||||
if (SetProperty(ref _selectedChangeNode, value)) {
|
||||
if (value == null) {
|
||||
SelectedChange = null;
|
||||
} else {
|
||||
SelectedChange = value.Backend as Models.Change;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SearchChangeFilter {
|
||||
get => _searchChangeFilter;
|
||||
set {
|
||||
if (SetProperty(ref _searchChangeFilter, value)) {
|
||||
RefreshVisibleChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileTreeNode> RevisionFilesTree {
|
||||
get => _revisionFilesTree;
|
||||
set => SetProperty(ref _revisionFilesTree, value);
|
||||
}
|
||||
|
||||
public FileTreeNode SelectedRevisionFileNode {
|
||||
get => _selectedRevisionFileNode;
|
||||
set {
|
||||
if (SetProperty(ref _selectedRevisionFileNode, value) && value != null && !value.IsFolder) {
|
||||
RefreshViewRevisionFile(value.Backend as Models.Object);
|
||||
} else {
|
||||
ViewRevisionFileContent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SearchFileFilter {
|
||||
get => _searchFileFilter;
|
||||
set {
|
||||
if (SetProperty(ref _searchFileFilter, value)) {
|
||||
RefreshVisibleFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object ViewRevisionFileContent {
|
||||
get => _viewRevisionFileContent;
|
||||
set => SetProperty(ref _viewRevisionFileContent, value);
|
||||
}
|
||||
|
||||
public CommitDetail(string repo) {
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public void NavigateTo(string commitSHA) {
|
||||
var repo = Preference.FindRepository(_repo);
|
||||
if (repo != null) repo.NavigateToCommit(commitSHA);
|
||||
}
|
||||
|
||||
public void ClearSearchChangeFilter() {
|
||||
SearchChangeFilter = string.Empty;
|
||||
}
|
||||
|
||||
public void ClearSearchFileFilter() {
|
||||
SearchFileFilter = string.Empty;
|
||||
}
|
||||
|
||||
public ContextMenu CreateChangeContextMenu(Models.Change change) {
|
||||
var menu = new ContextMenu();
|
||||
|
||||
if (change.Index != Models.ChangeState.Deleted) {
|
||||
var history = new MenuItem();
|
||||
history.Header = App.Text("FileHistory");
|
||||
history.Icon = CreateMenuIcon("Icons.Histories");
|
||||
history.Click += (_, ev) => {
|
||||
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
|
||||
window.Show();
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var blame = new MenuItem();
|
||||
blame.Header = App.Text("Blame");
|
||||
blame.Icon = CreateMenuIcon("Icons.Blame");
|
||||
blame.Click += (o, ev) => {
|
||||
var window = new Views.Blame() { DataContext = new Blame(_repo, change.Path, _commit.SHA) };
|
||||
window.Show();
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var full = Path.GetFullPath(Path.Combine(_repo, change.Path));
|
||||
var explore = new MenuItem();
|
||||
explore.Header = App.Text("RevealFile");
|
||||
explore.Icon = CreateMenuIcon("Icons.Folder.Open");
|
||||
explore.IsEnabled = File.Exists(full);
|
||||
explore.Click += (_, ev) => {
|
||||
Native.OS.OpenInFileManager(full, true);
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(history);
|
||||
menu.Items.Add(blame);
|
||||
menu.Items.Add(explore);
|
||||
}
|
||||
|
||||
var copyPath = new MenuItem();
|
||||
copyPath.Header = App.Text("CopyPath");
|
||||
copyPath.Icon = CreateMenuIcon("Icons.Copy");
|
||||
copyPath.Click += (_, ev) => {
|
||||
App.CopyText(change.Path);
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(copyPath);
|
||||
return menu;
|
||||
}
|
||||
|
||||
public ContextMenu CreateRevisionFileContextMenu(Models.Object file) {
|
||||
var history = new MenuItem();
|
||||
history.Header = App.Text("FileHistory");
|
||||
history.Icon = CreateMenuIcon("Icons.Histories");
|
||||
history.Click += (_, ev) => {
|
||||
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path) };
|
||||
window.Show();
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var blame = new MenuItem();
|
||||
blame.Header = App.Text("Blame");
|
||||
blame.Icon = CreateMenuIcon("Icons.Blame");
|
||||
blame.Click += (o, ev) => {
|
||||
var window = new Views.Blame() { DataContext = new Blame(_repo, file.Path, _commit.SHA) };
|
||||
window.Show();
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var full = Path.GetFullPath(Path.Combine(_repo, file.Path));
|
||||
var explore = new MenuItem();
|
||||
explore.Header = App.Text("RevealFile");
|
||||
explore.Icon = CreateMenuIcon("Icons.Folder.Open");
|
||||
explore.Click += (_, ev) => {
|
||||
Native.OS.OpenInFileManager(full, file.Type == Models.ObjectType.Blob);
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var saveAs = new MenuItem();
|
||||
saveAs.Header = App.Text("SaveAs");
|
||||
saveAs.Icon = CreateMenuIcon("Icons.Save");
|
||||
saveAs.IsEnabled = file.Type == Models.ObjectType.Blob;
|
||||
saveAs.Click += async (_, ev) => {
|
||||
var topLevel = App.GetTopLevel();
|
||||
if (topLevel == null) return;
|
||||
|
||||
var options = new FolderPickerOpenOptions() { AllowMultiple = false };
|
||||
var selected = await topLevel.StorageProvider.OpenFolderPickerAsync(options);
|
||||
if (selected.Count == 1) {
|
||||
var saveTo = Path.Combine(selected[0].Path.LocalPath, Path.GetFileName(file.Path));
|
||||
Commands.SaveRevisionFile.Run(_repo, _commit.SHA, file.Path, saveTo);
|
||||
}
|
||||
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var copyPath = new MenuItem();
|
||||
copyPath.Header = App.Text("CopyPath");
|
||||
copyPath.Icon = CreateMenuIcon("Icons.Copy");
|
||||
copyPath.Click += (_, ev) => {
|
||||
App.CopyText(file.Path);
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var menu = new ContextMenu();
|
||||
menu.Items.Add(history);
|
||||
menu.Items.Add(blame);
|
||||
menu.Items.Add(explore);
|
||||
menu.Items.Add(saveAs);
|
||||
menu.Items.Add(copyPath);
|
||||
return menu;
|
||||
}
|
||||
|
||||
private void Refresh() {
|
||||
_changes = null;
|
||||
VisibleChanges = null;
|
||||
SelectedChange = null;
|
||||
RevisionFilesTree = null;
|
||||
SelectedRevisionFileNode = null;
|
||||
if (_commit == null) return;
|
||||
if (_cancelToken != null) _cancelToken.Requested = true;
|
||||
|
||||
_cancelToken = new Commands.Command.CancelToken();
|
||||
var cmdChanges = new Commands.QueryCommitChanges(_repo, _commit.SHA) { Cancel = _cancelToken };
|
||||
var cmdRevisionFiles = new Commands.QueryRevisionObjects(_repo, _commit.SHA) { Cancel = _cancelToken };
|
||||
|
||||
Task.Run(() => {
|
||||
var changes = cmdChanges.Result();
|
||||
if (cmdChanges.Cancel.Requested) return;
|
||||
|
||||
var visible = changes;
|
||||
if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) {
|
||||
visible = new List<Models.Change>();
|
||||
foreach (var c in changes) {
|
||||
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) {
|
||||
visible.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tree = FileTreeNode.Build(visible);
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
Changes = changes;
|
||||
VisibleChanges = visible;
|
||||
ChangeTree = tree;
|
||||
});
|
||||
});
|
||||
|
||||
Task.Run(() => {
|
||||
var files = cmdRevisionFiles.Result();
|
||||
if (cmdRevisionFiles.Cancel.Requested) return;
|
||||
|
||||
var visible = files;
|
||||
if (!string.IsNullOrWhiteSpace(_searchFileFilter)) {
|
||||
visible = new List<Models.Object>();
|
||||
foreach (var f in files) {
|
||||
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) {
|
||||
visible.Add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tree = FileTreeNode.Build(visible);
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
_revisionFiles = files;
|
||||
RevisionFilesTree = tree;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void RefreshVisibleChanges() {
|
||||
if (_changes == null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(_searchChangeFilter)) {
|
||||
VisibleChanges = _changes;
|
||||
} else {
|
||||
var visible = new List<Models.Change>();
|
||||
foreach (var c in _changes) {
|
||||
if (c.Path.Contains(_searchChangeFilter, StringComparison.OrdinalIgnoreCase)) {
|
||||
visible.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
VisibleChanges = visible;
|
||||
}
|
||||
|
||||
ChangeTree = FileTreeNode.Build(_visibleChanges);
|
||||
}
|
||||
|
||||
private void RefreshVisibleFiles() {
|
||||
if (_revisionFiles == null) return;
|
||||
|
||||
var visible = _revisionFiles;
|
||||
if (!string.IsNullOrWhiteSpace(_searchFileFilter)) {
|
||||
visible = new List<Models.Object>();
|
||||
foreach (var f in _revisionFiles) {
|
||||
if (f.Path.Contains(_searchFileFilter, StringComparison.OrdinalIgnoreCase)) {
|
||||
visible.Add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
ViewRevisionFileContent = new Models.RevisionBinaryFile();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var content = new Commands.QueryFileContent(_repo, _commit.SHA, file.Path).Result();
|
||||
if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.OrdinalIgnoreCase)) {
|
||||
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:")) {
|
||||
obj.Object.Oid = line.Substring(11);
|
||||
} else if (line.StartsWith("size ")) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private Avalonia.Controls.Shapes.Path CreateMenuIcon(string key) {
|
||||
var icon = new Avalonia.Controls.Shapes.Path();
|
||||
icon.Width = 12;
|
||||
icon.Height = 12;
|
||||
icon.Stretch = Stretch.Uniform;
|
||||
icon.Data = App.Current?.FindResource(key) as StreamGeometry;
|
||||
return icon;
|
||||
}
|
||||
|
||||
private string _repo = string.Empty;
|
||||
private int _activePageIndex = 0;
|
||||
private Models.Commit _commit = null;
|
||||
private List<Models.Change> _changes = null;
|
||||
private List<Models.Change> _visibleChanges = null;
|
||||
private List<FileTreeNode> _changeTree = null;
|
||||
private Models.Change _selectedChange = null;
|
||||
private FileTreeNode _selectedChangeNode = null;
|
||||
private string _searchChangeFilter = string.Empty;
|
||||
private DiffContext _diffContext = null;
|
||||
private List<Models.Object> _revisionFiles = null;
|
||||
private List<FileTreeNode> _revisionFilesTree = null;
|
||||
private FileTreeNode _selectedRevisionFileNode = null;
|
||||
private string _searchFileFilter = string.Empty;
|
||||
private object _viewRevisionFileContent = null;
|
||||
private Commands.Command.CancelToken _cancelToken = null;
|
||||
}
|
||||
}
|
111
src/ViewModels/CreateBranch.cs
Normal file
111
src/ViewModels/CreateBranch.cs
Normal file
|
@ -0,0 +1,111 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value, true);
|
||||
}
|
||||
|
||||
public object BasedOn {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool CheckoutAfterCreated {
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
public bool AutoStash {
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
public CreateBranch(Repository repo, Models.Branch branch) {
|
||||
_repo = repo;
|
||||
_baseOnRevision = branch.Head;
|
||||
|
||||
BasedOn = branch;
|
||||
View = new Views.CreateBranch() { DataContext = this };
|
||||
}
|
||||
|
||||
public CreateBranch(Repository repo, Models.Commit commit) {
|
||||
_repo = repo;
|
||||
_baseOnRevision = commit.SHA;
|
||||
|
||||
BasedOn = commit;
|
||||
View = new Views.CreateBranch() { DataContext = this };
|
||||
}
|
||||
|
||||
public CreateBranch(Repository repo, Models.Tag tag) {
|
||||
_repo = repo;
|
||||
_baseOnRevision = tag.SHA;
|
||||
|
||||
BasedOn = tag;
|
||||
View = new Views.CreateBranch() { DataContext = this };
|
||||
}
|
||||
|
||||
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) {
|
||||
var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}";
|
||||
if (test == name) return new ValidationResult("A branch with same name already exists!");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
if (CheckoutAfterCreated) {
|
||||
bool needPopStash = false;
|
||||
if (_repo.WorkingCopyChangesCount > 0) {
|
||||
if (AutoStash) {
|
||||
SetProgressDescription("Adding untracked changes...");
|
||||
var succ = new Commands.Add(_repo.FullPath).Exec();
|
||||
if (succ) {
|
||||
SetProgressDescription("Stash local changes");
|
||||
succ = new Commands.Stash(_repo.FullPath).Push("CREATE_BRANCH_AUTO_STASH");
|
||||
}
|
||||
|
||||
if (!succ) {
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return false;
|
||||
}
|
||||
|
||||
needPopStash = true;
|
||||
} else {
|
||||
SetProgressDescription("Discard local changes...");
|
||||
Commands.Discard.All(_repo.FullPath);
|
||||
}
|
||||
}
|
||||
|
||||
SetProgressDescription($"Create new branch '{_name}'");
|
||||
new Commands.Checkout(_repo.FullPath).Branch(_name, _baseOnRevision, SetProgressDescription);
|
||||
|
||||
if (needPopStash) {
|
||||
SetProgressDescription("Re-apply local changes...");
|
||||
new Commands.Stash(_repo.FullPath).Pop("stash@{0}");
|
||||
}
|
||||
} else {
|
||||
Commands.Branch.Create(_repo.FullPath, _name, _baseOnRevision);
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _name = null;
|
||||
private string _baseOnRevision = null;
|
||||
}
|
||||
}
|
32
src/ViewModels/CreateGroup.cs
Normal file
32
src/ViewModels/CreateGroup.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class CreateGroup : Popup {
|
||||
[Required(ErrorMessage = "Group name is required!")]
|
||||
public string Name {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value, true);
|
||||
}
|
||||
|
||||
public CreateGroup(RepositoryNode parent) {
|
||||
_parent = parent;
|
||||
View = new Views.CreateGroup() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
Preference.AddNode(new RepositoryNode() {
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Name = _name,
|
||||
IsRepository = false,
|
||||
IsExpanded = false,
|
||||
}, _parent);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private RepositoryNode _parent = null;
|
||||
private string _name = string.Empty;
|
||||
}
|
||||
}
|
64
src/ViewModels/CreateTag.cs
Normal file
64
src/ViewModels/CreateTag.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 {
|
||||
get => _tagName;
|
||||
set => SetProperty(ref _tagName, value, true);
|
||||
}
|
||||
|
||||
public string Message {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public object BasedOn {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public CreateTag(Repository repo, Models.Branch branch) {
|
||||
_repo = repo;
|
||||
_basedOn = branch.Head;
|
||||
|
||||
BasedOn = branch;
|
||||
View = new Views.CreateTag() { DataContext = this };
|
||||
}
|
||||
|
||||
public CreateTag(Repository repo, Models.Commit commit) {
|
||||
_repo = repo;
|
||||
_basedOn = commit.SHA;
|
||||
|
||||
BasedOn = commit;
|
||||
View = new Views.CreateTag() { DataContext = this };
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateTagName(string name, ValidationContext ctx) {
|
||||
var creator = ctx.ObjectInstance as CreateTag;
|
||||
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() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
Commands.Tag.Add(_repo.FullPath, TagName, _basedOn, Message);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _tagName = string.Empty;
|
||||
private string _basedOn = string.Empty;
|
||||
}
|
||||
}
|
32
src/ViewModels/DeleteBranch.cs
Normal file
32
src/ViewModels/DeleteBranch.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class DeleteBranch : Popup {
|
||||
public Models.Branch Target {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public DeleteBranch(Repository repo, Models.Branch branch) {
|
||||
_repo = repo;
|
||||
Target = branch;
|
||||
View = new Views.DeleteBranch() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
if (Target.IsLocal) {
|
||||
Commands.Branch.Delete(_repo.FullPath, Target.Name);
|
||||
} else {
|
||||
new Commands.Push(_repo.FullPath, Target.Remote, Target.Name).Exec();
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
28
src/ViewModels/DeleteRemote.cs
Normal file
28
src/ViewModels/DeleteRemote.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class DeleteRemote : Popup {
|
||||
public Models.Remote Remote {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public DeleteRemote(Repository repo, Models.Remote remote) {
|
||||
_repo = repo;
|
||||
Remote = remote;
|
||||
View = new Views.DeleteRemote() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription("Deleting remote ...");
|
||||
var succ = new Commands.Remote(_repo.FullPath).Delete(Remote.Name);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
22
src/ViewModels/DeleteRepositoryNode.cs
Normal file
22
src/ViewModels/DeleteRepositoryNode.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class DeleteRepositoryNode : Popup {
|
||||
public RepositoryNode Node {
|
||||
get => _node;
|
||||
set => SetProperty(ref _node, value);
|
||||
}
|
||||
|
||||
public DeleteRepositoryNode(RepositoryNode node) {
|
||||
_node = node;
|
||||
View = new Views.DeleteRepositoryNode() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
Preference.RemoveNode(_node);
|
||||
return null;
|
||||
}
|
||||
|
||||
private RepositoryNode _node = null;
|
||||
}
|
||||
}
|
28
src/ViewModels/DeleteSubmodule.cs
Normal file
28
src/ViewModels/DeleteSubmodule.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class DeleteSubmodule : Popup {
|
||||
|
||||
public string Submodule {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public DeleteSubmodule(Repository repo, string submodule) {
|
||||
_repo = repo;
|
||||
Submodule = submodule;
|
||||
View = new Views.DeleteSubmodule() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
var succ = new Commands.Submodule(_repo.FullPath).Delete(Submodule);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
35
src/ViewModels/DeleteTag.cs
Normal file
35
src/ViewModels/DeleteTag.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class DeleteTag : Popup {
|
||||
public Models.Tag Target {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool ShouldPushToRemote {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public DeleteTag(Repository repo, Models.Tag tag) {
|
||||
_repo = repo;
|
||||
Target = tag;
|
||||
ShouldPushToRemote = true;
|
||||
View = new Views.DeleteTag() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Deleting tag '{Target.Name}' ...");
|
||||
var remotes = ShouldPushToRemote ? _repo.Remotes : null;
|
||||
var succ = Commands.Tag.Delete(_repo.FullPath, Target.Name, remotes);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
105
src/ViewModels/DiffContext.cs
Normal file
105
src/ViewModels/DiffContext.cs
Normal file
|
@ -0,0 +1,105 @@
|
|||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class DiffContext : ObservableObject {
|
||||
public string FilePath {
|
||||
get => _option.Path;
|
||||
}
|
||||
|
||||
public bool IsOrgFilePathVisible {
|
||||
get => !string.IsNullOrWhiteSpace(_option.OrgPath) && _option.OrgPath != "/dev/null";
|
||||
}
|
||||
|
||||
public string OrgFilePath {
|
||||
get => _option.OrgPath;
|
||||
}
|
||||
|
||||
public bool IsLoading {
|
||||
get => _isLoading;
|
||||
private set => SetProperty(ref _isLoading, value);
|
||||
}
|
||||
|
||||
public bool IsNoChange {
|
||||
get => _isNoChange;
|
||||
private set => SetProperty(ref _isNoChange, value);
|
||||
}
|
||||
|
||||
public bool IsTextDiff {
|
||||
get => _isTextDiff;
|
||||
private set => SetProperty(ref _isTextDiff, value);
|
||||
}
|
||||
|
||||
public object Content {
|
||||
get => _content;
|
||||
private set => SetProperty(ref _content, value);
|
||||
}
|
||||
|
||||
public DiffContext(string repo, Models.DiffOption option) {
|
||||
_repo = repo;
|
||||
_option = option;
|
||||
|
||||
OnPropertyChanged(nameof(FilePath));
|
||||
OnPropertyChanged(nameof(IsOrgFilePathVisible));
|
||||
OnPropertyChanged(nameof(OrgFilePath));
|
||||
|
||||
Task.Run(() => {
|
||||
var latest = new Commands.Diff(repo, option).Result();
|
||||
var binaryDiff = null as Models.BinaryDiff;
|
||||
|
||||
if (latest.IsBinary) {
|
||||
binaryDiff = new Models.BinaryDiff();
|
||||
|
||||
var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath;
|
||||
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 {
|
||||
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) {
|
||||
Content = binaryDiff;
|
||||
} else if (latest.IsLFS) {
|
||||
Content = latest.LFSDiff;
|
||||
} else if (latest.TextDiff != null) {
|
||||
latest.TextDiff.File = _option.Path;
|
||||
Content = latest.TextDiff;
|
||||
IsTextDiff = true;
|
||||
} else {
|
||||
IsTextDiff = false;
|
||||
IsNoChange = true;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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)) {
|
||||
App.RaiseException(_repo, "Invalid merge tool in preference setting!");
|
||||
return;
|
||||
}
|
||||
|
||||
var args = tool.Type != 0 ? tool.DiffCmd : Preference.Instance.ExternalMergeToolDiffCmd;
|
||||
await Task.Run(() => Commands.MergeTool.OpenForDiff(_repo, exec, args, _option));
|
||||
}
|
||||
|
||||
private string _repo = string.Empty;
|
||||
private Models.DiffOption _option = null;
|
||||
private bool _isLoading = true;
|
||||
private bool _isNoChange = false;
|
||||
private bool _isTextDiff = false;
|
||||
private object _content = null;
|
||||
}
|
||||
}
|
50
src/ViewModels/Discard.cs
Normal file
50
src/ViewModels/Discard.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 object Mode {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Discard(Repository repo, List<Models.Change> changes = null) {
|
||||
_repo = repo;
|
||||
_changes = changes;
|
||||
|
||||
if (_changes == null) {
|
||||
Mode = new DiscardModeAll();
|
||||
} else if (_changes.Count == 1) {
|
||||
Mode = new DiscardModeSingle() { File = _changes[0].Path };
|
||||
} else {
|
||||
Mode = new DiscardModeMulti() { Count = _changes.Count };
|
||||
}
|
||||
|
||||
View = new Views.Discard() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
if (_changes == null) {
|
||||
SetProgressDescription("Discard all local changes ...");
|
||||
Commands.Discard.All(_repo.FullPath);
|
||||
} else {
|
||||
SetProgressDescription($"Discard total {_changes.Count} changes ...");
|
||||
Commands.Discard.Changes(_repo.FullPath, _changes);
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private List<Models.Change> _changes = null;
|
||||
}
|
||||
}
|
22
src/ViewModels/DropStash.cs
Normal file
22
src/ViewModels/DropStash.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class DropStash : Popup {
|
||||
public Models.Stash Stash { get; private set; }
|
||||
|
||||
public DropStash(string repo, Models.Stash stash) {
|
||||
_repo = repo;
|
||||
Stash = stash;
|
||||
View = new Views.DropStash() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
return Task.Run(() => {
|
||||
new Commands.Stash(_repo).Drop(Stash.Name);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private string _repo;
|
||||
}
|
||||
}
|
100
src/ViewModels/EditRemote.cs
Normal file
100
src/ViewModels/EditRemote.cs
Normal file
|
@ -0,0 +1,100 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value, true);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Remote URL is required!!!")]
|
||||
[CustomValidation(typeof(EditRemote), nameof(ValidateRemoteURL))]
|
||||
public string Url {
|
||||
get => _url;
|
||||
set {
|
||||
if (SetProperty(ref _url, value, true)) UseSSH = Models.Remote.IsSSH(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseSSH {
|
||||
get => _useSSH;
|
||||
set => SetProperty(ref _useSSH, value);
|
||||
}
|
||||
|
||||
public string SSHKey {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
if (remote != edit._remote && name == remote.Name) new ValidationResult("A remote with given name already exists!!!");
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (remote != edit._remote && url == remote.URL) new ValidationResult("A remote with the same url already exists!!!");
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Editing remote '{_remote.Name}' ...");
|
||||
|
||||
if (_remote.Name != _name) {
|
||||
var succ = new Commands.Remote(_repo.FullPath).Rename(_remote.Name, _name);
|
||||
if (succ) _remote.Name = _name;
|
||||
}
|
||||
|
||||
if (_remote.URL != _url) {
|
||||
var succ = new Commands.Remote(_repo.FullPath).SetURL(_name, _url);
|
||||
if (succ) _remote.URL = _url;
|
||||
}
|
||||
|
||||
if (_useSSH) {
|
||||
SetProgressDescription("Post processing ...");
|
||||
new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", SSHKey);
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private Models.Remote _remote = null;
|
||||
private string _name = string.Empty;
|
||||
private string _url = string.Empty;
|
||||
private bool _useSSH = false;
|
||||
}
|
||||
}
|
55
src/ViewModels/EditRepositoryNode.cs
Normal file
55
src/ViewModels/EditRepositoryNode.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class EditRepositoryNode : Popup {
|
||||
public RepositoryNode Node {
|
||||
get => _node;
|
||||
set => SetProperty(ref _node, value);
|
||||
}
|
||||
|
||||
public string Id {
|
||||
get => _id;
|
||||
set => SetProperty(ref _id, value);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Name is required!")]
|
||||
public string Name {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value, true);
|
||||
}
|
||||
|
||||
public int Bookmark {
|
||||
get => _bookmark;
|
||||
set => SetProperty(ref _bookmark, value);
|
||||
}
|
||||
|
||||
public bool IsRepository {
|
||||
get => _isRepository;
|
||||
set => SetProperty(ref _isRepository, value);
|
||||
}
|
||||
|
||||
public EditRepositoryNode(RepositoryNode node) {
|
||||
_node = node;
|
||||
_id = node.Id;
|
||||
_name = node.Name;
|
||||
_isRepository = node.IsRepository;
|
||||
_bookmark = node.Bookmark;
|
||||
|
||||
View = new Views.EditRepositoryNode() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_node.Name = _name;
|
||||
_node.Bookmark = _bookmark;
|
||||
return null;
|
||||
}
|
||||
|
||||
private RepositoryNode _node = null;
|
||||
private string _id = string.Empty;
|
||||
private string _name = string.Empty;
|
||||
private bool _isRepository = false;
|
||||
private int _bookmark = 0;
|
||||
}
|
||||
}
|
34
src/ViewModels/FastForwardWithoutCheckout.cs
Normal file
34
src/ViewModels/FastForwardWithoutCheckout.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class FastForwardWithoutCheckout : Popup {
|
||||
public Models.Branch Local {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Models.Branch To {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
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() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription("Fast-Forward ...");
|
||||
new Commands.Fetch(_repo.FullPath, To.Remote, Local.Name, To.Name, SetProgressDescription).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
54
src/ViewModels/Fetch.cs
Normal file
54
src/ViewModels/Fetch.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Fetch : Popup {
|
||||
public List<Models.Remote> Remotes {
|
||||
get => _repo.Remotes;
|
||||
}
|
||||
|
||||
public bool FetchAllRemotes {
|
||||
get => _fetchAllRemotes;
|
||||
set => SetProperty(ref _fetchAllRemotes, value);
|
||||
}
|
||||
|
||||
public Models.Remote SelectedRemote {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool Prune {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Fetch(Repository repo, Models.Remote preferedRemote = null) {
|
||||
_repo = repo;
|
||||
_fetchAllRemotes = true;
|
||||
SelectedRemote = preferedRemote != null ? preferedRemote : _repo.Remotes[0];
|
||||
Prune = true;
|
||||
View = new Views.Fetch() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
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 {
|
||||
SetProgressDescription($"Fetching remote: {SelectedRemote.Name}");
|
||||
new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, Prune, SetProgressDescription).Exec();
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private bool _fetchAllRemotes = true;
|
||||
}
|
||||
}
|
66
src/ViewModels/FileHistories.cs
Normal file
66
src/ViewModels/FileHistories.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class FileHistories : ObservableObject {
|
||||
public string File {
|
||||
get => _file;
|
||||
}
|
||||
|
||||
public bool IsLoading {
|
||||
get => _isLoading;
|
||||
private set => SetProperty(ref _isLoading, value);
|
||||
}
|
||||
|
||||
public List<Models.Commit> Commits {
|
||||
get => _commits;
|
||||
set => SetProperty(ref _commits, value);
|
||||
}
|
||||
|
||||
public Models.Commit SelectedCommit {
|
||||
get => _selectedCommit;
|
||||
set {
|
||||
if (SetProperty(ref _selectedCommit, value)) {
|
||||
if (value == null) {
|
||||
DiffContext = null;
|
||||
} else {
|
||||
DiffContext = new DiffContext(_repo, new Models.DiffOption(value, _file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DiffContext DiffContext {
|
||||
get => _diffContext;
|
||||
set => SetProperty(ref _diffContext, value);
|
||||
}
|
||||
|
||||
public FileHistories(string repo, string file) {
|
||||
_repo = repo;
|
||||
_file = file;
|
||||
|
||||
Task.Run(() => {
|
||||
var commits = new Commands.QueryCommits(_repo, $"-n 10000 -- \"{file}\"").Result();
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
IsLoading = false;
|
||||
Commits = commits;
|
||||
if (commits.Count > 0) SelectedCommit = commits[0];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 bool _isLoading = true;
|
||||
private List<Models.Commit> _commits = null;
|
||||
private Models.Commit _selectedCommit = null;
|
||||
private DiffContext _diffContext = null;
|
||||
}
|
||||
}
|
170
src/ViewModels/FileTreeNode.cs
Normal file
170
src/ViewModels/FileTreeNode.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
|
||||
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 {
|
||||
get => _isExpanded;
|
||||
set => SetProperty(ref _isExpanded, value);
|
||||
}
|
||||
|
||||
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) {
|
||||
var sepIdx = c.Path.IndexOf('/');
|
||||
if (sepIdx == -1) {
|
||||
nodes.Add(new FileTreeNode() {
|
||||
FullPath = c.Path,
|
||||
Backend = c,
|
||||
IsFolder = false,
|
||||
IsExpanded = false
|
||||
});
|
||||
} else {
|
||||
FileTreeNode lastFolder = null;
|
||||
var start = 0;
|
||||
|
||||
while (sepIdx != -1) {
|
||||
var folder = c.Path.Substring(0, sepIdx);
|
||||
if (folders.ContainsKey(folder)) {
|
||||
lastFolder = folders[folder];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new FileTreeNode() {
|
||||
FullPath = folder,
|
||||
Backend = null,
|
||||
IsFolder = true,
|
||||
IsExpanded = expanded
|
||||
};
|
||||
nodes.Add(lastFolder);
|
||||
folders.Add(folder, lastFolder);
|
||||
} else {
|
||||
var cur = new FileTreeNode() {
|
||||
FullPath = folder,
|
||||
Backend = null,
|
||||
IsFolder = true,
|
||||
IsExpanded = expanded
|
||||
};
|
||||
folders.Add(folder, cur);
|
||||
lastFolder.Children.Add(cur);
|
||||
lastFolder = cur;
|
||||
}
|
||||
|
||||
start = sepIdx + 1;
|
||||
sepIdx = c.Path.IndexOf('/', start);
|
||||
}
|
||||
|
||||
lastFolder.Children.Add(new FileTreeNode() {
|
||||
FullPath = c.Path,
|
||||
Backend = c,
|
||||
IsFolder = false,
|
||||
IsExpanded = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
folders.Clear();
|
||||
Sort(nodes);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
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) {
|
||||
var sepIdx = f.Path.IndexOf('/');
|
||||
if (sepIdx == -1) {
|
||||
nodes.Add(new FileTreeNode() {
|
||||
FullPath = f.Path,
|
||||
Backend = f,
|
||||
IsFolder = false,
|
||||
IsExpanded = false
|
||||
});
|
||||
} else {
|
||||
FileTreeNode lastFolder = null;
|
||||
var start = 0;
|
||||
|
||||
while (sepIdx != -1) {
|
||||
var folder = f.Path.Substring(0, sepIdx);
|
||||
if (folders.ContainsKey(folder)) {
|
||||
lastFolder = folders[folder];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new FileTreeNode() {
|
||||
FullPath = folder,
|
||||
Backend = null,
|
||||
IsFolder = true,
|
||||
IsExpanded = expanded
|
||||
};
|
||||
nodes.Add(lastFolder);
|
||||
folders.Add(folder, lastFolder);
|
||||
} else {
|
||||
var cur = new FileTreeNode() {
|
||||
FullPath = folder,
|
||||
Backend = null,
|
||||
IsFolder = true,
|
||||
IsExpanded = expanded
|
||||
};
|
||||
folders.Add(folder, cur);
|
||||
lastFolder.Children.Add(cur);
|
||||
lastFolder = cur;
|
||||
}
|
||||
|
||||
start = sepIdx + 1;
|
||||
sepIdx = f.Path.IndexOf('/', start);
|
||||
}
|
||||
|
||||
lastFolder.Children.Add(new FileTreeNode() {
|
||||
FullPath = f.Path,
|
||||
Backend = f,
|
||||
IsFolder = false,
|
||||
IsExpanded = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
folders.Clear();
|
||||
Sort(nodes);
|
||||
return 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 + "/")) {
|
||||
var foundInChildren = SelectByPath(node.Children, path);
|
||||
if (foundInChildren != null) {
|
||||
node.IsExpanded = true;
|
||||
}
|
||||
return foundInChildren;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void Sort(List<FileTreeNode> nodes) {
|
||||
nodes.Sort((l, r) => {
|
||||
if (l.IsFolder == r.IsFolder) {
|
||||
return l.FullPath.CompareTo(r.FullPath);
|
||||
} else {
|
||||
return l.IsFolder ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var node in nodes) {
|
||||
if (node.Children.Count > 1) Sort(node.Children);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isExpanded = true;
|
||||
}
|
||||
}
|
48
src/ViewModels/GitFlowFinish.cs
Normal file
48
src/ViewModels/GitFlowFinish.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
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 {
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
|
||||
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() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
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;
|
||||
}
|
||||
|
||||
var succ = new Commands.GitFlow(_repo.FullPath).Finish(_type, branch, KeepBranch);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private Models.Branch _branch = null;
|
||||
private Models.GitFlowBranchType _type = Models.GitFlowBranchType.None;
|
||||
}
|
||||
}
|
67
src/ViewModels/GitFlowStart.cs
Normal file
67
src/ViewModels/GitFlowStart.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value, true);
|
||||
}
|
||||
|
||||
public string Prefix {
|
||||
get => _prefix;
|
||||
}
|
||||
|
||||
public bool IsFeature => _type == Models.GitFlowBranchType.Feature;
|
||||
public bool IsRelease => _type == Models.GitFlowBranchType.Release;
|
||||
public bool IsHotfix => _type == Models.GitFlowBranchType.Hotfix;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
View = new Views.GitFlowStart() { DataContext = this };
|
||||
}
|
||||
|
||||
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) {
|
||||
var test = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}";
|
||||
if (test == check) return new ValidationResult("A branch with same name already exists!");
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
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 string _name = null;
|
||||
}
|
||||
}
|
473
src/ViewModels/Histories.cs
Normal file
473
src/ViewModels/Histories.cs
Normal file
|
@ -0,0 +1,473 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class CountSelectedCommits {
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class Histories : ObservableObject {
|
||||
public bool IsLoading {
|
||||
get => _isLoading;
|
||||
set => SetProperty(ref _isLoading, value);
|
||||
}
|
||||
|
||||
public double DataGridRowHeight {
|
||||
get => _dataGridRowHeight;
|
||||
}
|
||||
|
||||
public List<Models.Commit> Commits {
|
||||
get => _commits;
|
||||
set {
|
||||
if (SetProperty(ref _commits, value)) {
|
||||
Graph = null;
|
||||
Task.Run(() => {
|
||||
var graph = Models.CommitGraph.Parse(value, DataGridRowHeight, 8);
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
Graph = graph;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Models.CommitGraph Graph {
|
||||
get => _graph;
|
||||
set => SetProperty(ref _graph, value);
|
||||
}
|
||||
|
||||
public Models.Commit AutoSelectedCommit {
|
||||
get => _autoSelectedCommit;
|
||||
private set => SetProperty(ref _autoSelectedCommit, value);
|
||||
}
|
||||
|
||||
public object DetailContext {
|
||||
get => _detailContext;
|
||||
private set => SetProperty(ref _detailContext, value);
|
||||
}
|
||||
|
||||
public Histories(Repository repo) {
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public void NavigateTo(string commitSHA) {
|
||||
var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA));
|
||||
if (commit != null) {
|
||||
AutoSelectedCommit = commit;
|
||||
|
||||
if (_detailContext is CommitDetail detail) {
|
||||
detail.Commit = commit;
|
||||
} else {
|
||||
var commitDetail = new CommitDetail(_repo.FullPath);
|
||||
commitDetail.Commit = commit;
|
||||
DetailContext = commitDetail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Select(IList commits) {
|
||||
if (commits.Count == 0) {
|
||||
DetailContext = null;
|
||||
} else if (commits.Count == 1) {
|
||||
var commit = commits[0] as Models.Commit;
|
||||
AutoSelectedCommit = commit;
|
||||
|
||||
if (_detailContext is CommitDetail detail) {
|
||||
detail.Commit = commit;
|
||||
} else {
|
||||
var commitDetail = new CommitDetail(_repo.FullPath);
|
||||
commitDetail.Commit = commit;
|
||||
DetailContext = commitDetail;
|
||||
}
|
||||
} 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 {
|
||||
DetailContext = new CountSelectedCommits() { Count = commits.Count };
|
||||
}
|
||||
}
|
||||
|
||||
public ContextMenu MakeContextMenu() {
|
||||
var detail = _detailContext as CommitDetail;
|
||||
if (detail == null) return null;
|
||||
|
||||
var current = _repo.Branches.Find(x => x.IsCurrent);
|
||||
if (current == null) return null;
|
||||
|
||||
var commit = detail.Commit;
|
||||
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) {
|
||||
FillCurrentBranchMenu(menu, current);
|
||||
} 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) {
|
||||
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) {
|
||||
var t = _repo.Tags.Find(x => x.Name == d.Name);
|
||||
if (t != null) tags.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.Items.Count > 0) menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
}
|
||||
|
||||
if (tags.Count > 0) {
|
||||
foreach (var tag in tags) FillTagMenu(menu, tag);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
}
|
||||
|
||||
if (current.Head != commit.SHA) {
|
||||
var reset = new MenuItem();
|
||||
reset.Header = App.Text("CommitCM.Reset", current.Name);
|
||||
reset.Icon = CreateMenuIcon("Icons.Reset");
|
||||
reset.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Reset(_repo, current, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(reset);
|
||||
} else {
|
||||
var reword = new MenuItem();
|
||||
reword.Header = App.Text("CommitCM.Reword");
|
||||
reword.Icon = CreateMenuIcon("Icons.Edit");
|
||||
reword.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Reword(_repo, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(reword);
|
||||
|
||||
var squash = new MenuItem();
|
||||
squash.Header = App.Text("CommitCM.Squash");
|
||||
squash.Icon = CreateMenuIcon("Icons.SquashIntoParent");
|
||||
squash.IsEnabled = 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) {
|
||||
var rebase = new MenuItem();
|
||||
rebase.Header = App.Text("CommitCM.Rebase", current.Name);
|
||||
rebase.Icon = CreateMenuIcon("Icons.Rebase");
|
||||
rebase.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Rebase(_repo, current, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(rebase);
|
||||
|
||||
var cherryPick = new MenuItem();
|
||||
cherryPick.Header = App.Text("CommitCM.CherryPick");
|
||||
cherryPick.Icon = CreateMenuIcon("Icons.CherryPick");
|
||||
cherryPick.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CherryPick(_repo, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(cherryPick);
|
||||
} else {
|
||||
var revert = new MenuItem();
|
||||
revert.Header = App.Text("CommitCM.Revert");
|
||||
revert.Icon = CreateMenuIcon("Icons.Undo");
|
||||
revert.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Revert(_repo, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(revert);
|
||||
}
|
||||
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
|
||||
var createBranch = new MenuItem();
|
||||
createBranch.Icon = CreateMenuIcon("Icons.Branch.Add");
|
||||
createBranch.Header = App.Text("CreateBranch");
|
||||
createBranch.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateBranch(_repo, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(createBranch);
|
||||
|
||||
var createTag = new MenuItem();
|
||||
createTag.Icon = CreateMenuIcon("Icons.Tag.Add");
|
||||
createTag.Header = App.Text("CreateTag");
|
||||
createTag.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateTag(_repo, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(createTag);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
|
||||
var saveToPatch = new MenuItem();
|
||||
saveToPatch.Icon = CreateMenuIcon("Icons.Diff");
|
||||
saveToPatch.Header = App.Text("CommitCM.SaveAsPatch");
|
||||
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) {
|
||||
var succ = new Commands.FormatPatch(_repo.FullPath, commit.SHA, selected[0].Path.LocalPath).Exec();
|
||||
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(saveToPatch);
|
||||
|
||||
var archive = new MenuItem();
|
||||
archive.Icon = CreateMenuIcon("Icons.Archive");
|
||||
archive.Header = App.Text("Archive");
|
||||
archive.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Archive(_repo, commit));
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(archive);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
|
||||
var copySHA = new MenuItem();
|
||||
copySHA.Header = App.Text("CommitCM.CopySHA");
|
||||
copySHA.Icon = CreateMenuIcon("Icons.Copy");
|
||||
copySHA.Click += (o, e) => {
|
||||
App.CopyText(commit.SHA);
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(copySHA);
|
||||
return menu;
|
||||
}
|
||||
|
||||
private void FillCurrentBranchMenu(ContextMenu menu, Models.Branch current) {
|
||||
var submenu = new MenuItem();
|
||||
submenu.Icon = CreateMenuIcon("Icons.Branch");
|
||||
submenu.Header = current.Name;
|
||||
|
||||
var dirty = !string.IsNullOrEmpty(current.UpstreamTrackStatus);
|
||||
if (!string.IsNullOrEmpty(current.Upstream)) {
|
||||
var upstream = current.Upstream.Substring(13);
|
||||
|
||||
var fastForward = new MenuItem();
|
||||
fastForward.Header = App.Text("BranchCM.FastForward", upstream);
|
||||
fastForward.Icon = CreateMenuIcon("Icons.FastForward");
|
||||
fastForward.IsEnabled = dirty;
|
||||
fastForward.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowAndStartPopup(new Merge(_repo, upstream, current.Name));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(fastForward);
|
||||
|
||||
var pull = new MenuItem();
|
||||
pull.Header = App.Text("BranchCM.Pull", upstream);
|
||||
pull.Icon = CreateMenuIcon("Icons.Pull");
|
||||
pull.IsEnabled = dirty;
|
||||
pull.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Pull(_repo, null));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(pull);
|
||||
}
|
||||
|
||||
var push = new MenuItem();
|
||||
push.Header = App.Text("BranchCM.Push", current.Name);
|
||||
push.Icon = CreateMenuIcon("Icons.Push");
|
||||
push.IsEnabled = _repo.Remotes.Count > 0 && dirty;
|
||||
push.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Push(_repo, current));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(push);
|
||||
submenu.Items.Add(new MenuItem() { Header = "-" });
|
||||
|
||||
var type = _repo.GitFlow.GetBranchType(current.Name);
|
||||
if (type != Models.GitFlowBranchType.None) {
|
||||
var finish = new MenuItem();
|
||||
finish.Header = App.Text("BranchCM.Finish", current.Name);
|
||||
finish.Icon = CreateMenuIcon("Icons.Flow");
|
||||
finish.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new GitFlowFinish(_repo, current, type));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(finish);
|
||||
submenu.Items.Add(new MenuItem() { Header = "-" });
|
||||
}
|
||||
|
||||
var rename = new MenuItem();
|
||||
rename.Header = App.Text("BranchCM.Rename", current.Name);
|
||||
rename.Icon = CreateMenuIcon("Icons.Rename");
|
||||
rename.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new RenameBranch(_repo, current));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(rename);
|
||||
|
||||
menu.Items.Add(submenu);
|
||||
}
|
||||
|
||||
private void FillOtherLocalBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) {
|
||||
var submenu = new MenuItem();
|
||||
submenu.Icon = CreateMenuIcon("Icons.Branch");
|
||||
submenu.Header = branch.Name;
|
||||
|
||||
var checkout = new MenuItem();
|
||||
checkout.Header = App.Text("BranchCM.Checkout", branch.Name);
|
||||
checkout.Icon = CreateMenuIcon("Icons.Check");
|
||||
checkout.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowAndStartPopup(new Checkout(_repo, branch.Name));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(checkout);
|
||||
|
||||
var merge = new MenuItem();
|
||||
merge.Header = App.Text("BranchCM.Merge", branch.Name, current.Name);
|
||||
merge.Icon = CreateMenuIcon("Icons.Merge");
|
||||
merge.IsEnabled = !merged;
|
||||
merge.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Merge(_repo, branch.Name, current.Name));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(merge);
|
||||
submenu.Items.Add(new MenuItem() { Header = "-" });
|
||||
|
||||
var type = _repo.GitFlow.GetBranchType(branch.Name);
|
||||
if (type != Models.GitFlowBranchType.None) {
|
||||
var finish = new MenuItem();
|
||||
finish.Header = App.Text("BranchCM.Finish", branch.Name);
|
||||
finish.Icon = CreateMenuIcon("Icons.Flow");
|
||||
finish.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new GitFlowFinish(_repo, branch, type));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(finish);
|
||||
submenu.Items.Add(new MenuItem() { Header = "-" });
|
||||
}
|
||||
|
||||
var rename = new MenuItem();
|
||||
rename.Header = App.Text("BranchCM.Rename", branch.Name);
|
||||
rename.Icon = CreateMenuIcon("Icons.Rename");
|
||||
rename.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new RenameBranch(_repo, branch));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(rename);
|
||||
|
||||
var delete = new MenuItem();
|
||||
delete.Header = App.Text("BranchCM.Delete", branch.Name);
|
||||
delete.Icon = CreateMenuIcon("Icons.Clear");
|
||||
delete.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteBranch(_repo, branch));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(delete);
|
||||
|
||||
menu.Items.Add(submenu);
|
||||
}
|
||||
|
||||
private void FillRemoteBranchMenu(ContextMenu menu, Models.Branch branch, Models.Branch current, bool merged) {
|
||||
var name = $"{branch.Remote}/{branch.Name}";
|
||||
|
||||
var submenu = new MenuItem();
|
||||
submenu.Icon = CreateMenuIcon("Icons.Branch");
|
||||
submenu.Header = name;
|
||||
|
||||
var checkout = new MenuItem();
|
||||
checkout.Header = App.Text("BranchCM.Checkout", name);
|
||||
checkout.Icon = CreateMenuIcon("Icons.Check");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateBranch(_repo, branch));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(checkout);
|
||||
|
||||
var merge = new MenuItem();
|
||||
merge.Header = App.Text("BranchCM.Merge", name, current.Name);
|
||||
merge.Icon = CreateMenuIcon("Icons.Merge");
|
||||
merge.IsEnabled = !merged;
|
||||
merge.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new Merge(_repo, name, current.Name));
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
submenu.Items.Add(merge);
|
||||
submenu.Items.Add(new MenuItem() { Header = "-" });
|
||||
|
||||
var delete = new MenuItem();
|
||||
delete.Header = App.Text("BranchCM.Delete", name);
|
||||
delete.Icon = CreateMenuIcon("Icons.Clear");
|
||||
delete.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteBranch(_repo, branch));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(delete);
|
||||
|
||||
menu.Items.Add(submenu);
|
||||
}
|
||||
|
||||
private void FillTagMenu(ContextMenu menu, Models.Tag tag) {
|
||||
var submenu = new MenuItem();
|
||||
submenu.Header = tag.Name;
|
||||
submenu.Icon = CreateMenuIcon("Icons.Tag");
|
||||
submenu.MinWidth = 200;
|
||||
|
||||
var push = new MenuItem();
|
||||
push.Header = App.Text("TagCM.Push", tag.Name);
|
||||
push.Icon = CreateMenuIcon("Icons.Push");
|
||||
push.IsEnabled = _repo.Remotes.Count > 0;
|
||||
push.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new PushTag(_repo, tag));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(push);
|
||||
|
||||
var delete = new MenuItem();
|
||||
delete.Header = App.Text("TagCM.Delete", tag.Name);
|
||||
delete.Icon = CreateMenuIcon("Icons.Clear");
|
||||
delete.Click += (o, e) => {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteTag(_repo, tag));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(delete);
|
||||
|
||||
menu.Items.Add(submenu);
|
||||
}
|
||||
|
||||
private Avalonia.Controls.Shapes.Path CreateMenuIcon(string key) {
|
||||
var icon = new Avalonia.Controls.Shapes.Path();
|
||||
icon.Width = 12;
|
||||
icon.Height = 12;
|
||||
icon.Stretch = Stretch.Uniform;
|
||||
icon.Data = App.Current?.FindResource(key) as StreamGeometry;
|
||||
return icon;
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private 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;
|
||||
}
|
||||
}
|
41
src/ViewModels/Init.cs
Normal file
41
src/ViewModels/Init.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Init : Popup {
|
||||
public string TargetPath {
|
||||
get => _targetPath;
|
||||
set => SetProperty(ref _targetPath, value);
|
||||
}
|
||||
|
||||
public Init(string path) {
|
||||
TargetPath = path;
|
||||
View = new Views.Init() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Initialize git repository at: '{_targetPath}'");
|
||||
var succ = new Commands.Init(HostPageId, _targetPath).Exec();
|
||||
if (!succ) return false;
|
||||
|
||||
var gitDir = Path.GetFullPath(Path.Combine(_targetPath, ".git"));
|
||||
|
||||
CallUIThread(() => {
|
||||
var repo = Preference.AddRepository(_targetPath, gitDir);
|
||||
var node = new RepositoryNode() {
|
||||
Id = _targetPath,
|
||||
Name = Path.GetFileName(_targetPath),
|
||||
Bookmark = 0,
|
||||
IsRepository = true,
|
||||
};
|
||||
Preference.AddNode(node);
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private string _targetPath;
|
||||
}
|
||||
}
|
96
src/ViewModels/InitGitFlow.cs
Normal file
96
src/ViewModels/InitGitFlow.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class InitGitFlow : Popup {
|
||||
private static readonly Regex TAG_PREFIX = new Regex(@"^[\w\-/\.]+$");
|
||||
|
||||
[Required(ErrorMessage = "Master branch name is required!!!")]
|
||||
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
|
||||
[CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))]
|
||||
public string Master {
|
||||
get => _master;
|
||||
set => SetProperty(ref _master, value, true);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Develop branch name is required!!!")]
|
||||
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
|
||||
[CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))]
|
||||
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 {
|
||||
get => _featurePrefix;
|
||||
set => SetProperty(ref _featurePrefix, value, true);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Release prefix is required!!!")]
|
||||
[RegularExpression(@"^[\w\-\.]+/$", ErrorMessage = "Bad release prefix format!")]
|
||||
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 {
|
||||
get => _hotfixPrefix;
|
||||
set => SetProperty(ref _hotfixPrefix, value, true);
|
||||
}
|
||||
|
||||
[CustomValidation(typeof(InitGitFlow), nameof(ValidateTagPrefix))]
|
||||
public string TagPrefix {
|
||||
get => _tagPrefix;
|
||||
set => SetProperty(ref _tagPrefix, value, true);
|
||||
}
|
||||
|
||||
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) {
|
||||
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)) {
|
||||
return new ValidationResult("Bad tag prefix format!");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
var succ = new Commands.GitFlow(_repo.FullPath).Init(_repo.Branches, _master, _develop, _featurePrefix, _releasePrefix, _hotfixPrefix, _tagPrefix);
|
||||
if (succ) {
|
||||
_repo.GitFlow.Feature = _featurePrefix;
|
||||
_repo.GitFlow.Release = _releasePrefix;
|
||||
_repo.GitFlow.Hotfix = _hotfixPrefix;
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _master = "master";
|
||||
private string _develop = "develop";
|
||||
private string _featurePrefix = "feature/";
|
||||
private string _releasePrefix = "release/";
|
||||
private string _hotfixPrefix = "hotfix/";
|
||||
private string _tagPrefix = string.Empty;
|
||||
}
|
||||
}
|
154
src/ViewModels/Launcher.cs
Normal file
154
src/ViewModels/Launcher.cs
Normal file
|
@ -0,0 +1,154 @@
|
|||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.IO;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Launcher : ObservableObject {
|
||||
public AvaloniaList<LauncherPage> Pages {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public LauncherPage ActivePage {
|
||||
get => _activePage;
|
||||
set {
|
||||
if (SetProperty(ref _activePage, value)) {
|
||||
PopupHost.Active = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Launcher() {
|
||||
Pages = new AvaloniaList<LauncherPage>();
|
||||
AddNewTab();
|
||||
}
|
||||
|
||||
public void AddNewTab() {
|
||||
var page = new LauncherPage();
|
||||
Pages.Add(page);
|
||||
ActivePage = page;
|
||||
}
|
||||
|
||||
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() {
|
||||
if (Pages.Count == 1) return;
|
||||
|
||||
var activeIdx = Pages.IndexOf(_activePage);
|
||||
var nextIdx = (activeIdx + 1) % Pages.Count;
|
||||
ActivePage = Pages[nextIdx];
|
||||
}
|
||||
|
||||
public void CloseTab(object param) {
|
||||
if (Pages.Count == 1) {
|
||||
App.Quit();
|
||||
return;
|
||||
}
|
||||
|
||||
LauncherPage page = param as LauncherPage;
|
||||
if (page == null) page = _activePage;
|
||||
|
||||
CloseRepositoryInTab(page);
|
||||
|
||||
var removeIdx = Pages.IndexOf(page);
|
||||
var activeIdx = Pages.IndexOf(_activePage);
|
||||
if (removeIdx == activeIdx) {
|
||||
if (removeIdx == Pages.Count - 1) {
|
||||
ActivePage = Pages[removeIdx - 1];
|
||||
} else {
|
||||
ActivePage = Pages[removeIdx + 1];
|
||||
}
|
||||
|
||||
Pages.RemoveAt(removeIdx);
|
||||
OnPropertyChanged(nameof(Pages));
|
||||
} else if (removeIdx + 1 == activeIdx) {
|
||||
Pages.RemoveAt(removeIdx);
|
||||
OnPropertyChanged(nameof(Pages));
|
||||
} else {
|
||||
Pages.RemoveAt(removeIdx);
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseOtherTabs(object param) {
|
||||
if (Pages.Count == 1) return;
|
||||
|
||||
LauncherPage page = param as LauncherPage;
|
||||
if (page == null) page = _activePage;
|
||||
|
||||
foreach (var one in Pages) {
|
||||
if (one.Node.Id != page.Node.Id) {
|
||||
CloseRepositoryInTab(one);
|
||||
}
|
||||
}
|
||||
|
||||
ActivePage = page;
|
||||
Pages = new AvaloniaList<LauncherPage> { page };
|
||||
OnPropertyChanged(nameof(Pages));
|
||||
}
|
||||
|
||||
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) {
|
||||
ActivePage = page;
|
||||
}
|
||||
|
||||
for (var i = Pages.Count - 1; i > endIdx; i--) {
|
||||
CloseRepositoryInTab(Pages[i]);
|
||||
Pages.Remove(Pages[i]);
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
var ctx = page == null ? ActivePage.Node.Id : page.Node.Id;
|
||||
App.RaiseException(ctx, "Repository does NOT exists any more. Please remove it.");
|
||||
return;
|
||||
}
|
||||
|
||||
repo.Open();
|
||||
|
||||
if (page == null) {
|
||||
if (ActivePage == null || ActivePage.Node.IsRepository) {
|
||||
page = new LauncherPage(node, repo);
|
||||
Pages.Add(page);
|
||||
} else {
|
||||
page.Node = node;
|
||||
page.View = new Views.Repository() { DataContext = repo };
|
||||
}
|
||||
} else {
|
||||
page.Node = node;
|
||||
page.View = new Views.Repository() { DataContext = repo };
|
||||
}
|
||||
|
||||
ActivePage = page;
|
||||
}
|
||||
|
||||
private void CloseRepositoryInTab(LauncherPage page) {
|
||||
if (!page.Node.IsRepository) return;
|
||||
|
||||
var repo = Preference.FindRepository(page.Node.Id);
|
||||
if (repo == null) return;
|
||||
|
||||
repo.Close();
|
||||
}
|
||||
|
||||
private LauncherPage _activePage = null;
|
||||
}
|
||||
}
|
53
src/ViewModels/LauncherPage.cs
Normal file
53
src/ViewModels/LauncherPage.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using Avalonia.Collections;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class LauncherPage : PopupHost {
|
||||
public RepositoryNode Node {
|
||||
get => _node;
|
||||
set => SetProperty(ref _node, value);
|
||||
}
|
||||
|
||||
public object View {
|
||||
get => _view;
|
||||
set => SetProperty(ref _view, value);
|
||||
}
|
||||
|
||||
public AvaloniaList<Models.Notification> Notifications {
|
||||
get;
|
||||
set;
|
||||
} = new AvaloniaList<Models.Notification>();
|
||||
|
||||
public LauncherPage() {
|
||||
_node = new RepositoryNode() {
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Name = "WelcomePage",
|
||||
Bookmark = 0,
|
||||
IsRepository = false,
|
||||
};
|
||||
_view = new Views.Welcome() { DataContext = new Welcome() };
|
||||
}
|
||||
|
||||
public LauncherPage(RepositoryNode node, Repository repo) {
|
||||
_node = node;
|
||||
_view = new Views.Repository() { DataContext = repo };
|
||||
}
|
||||
|
||||
public override string GetId() {
|
||||
return _node.Id;
|
||||
}
|
||||
|
||||
public void CopyPath() {
|
||||
if (_node.IsRepository) App.CopyText(_node.Id);
|
||||
}
|
||||
|
||||
public void DismissNotification(object param) {
|
||||
if (param is Models.Notification notice) {
|
||||
Notifications.Remove(notice);
|
||||
}
|
||||
}
|
||||
|
||||
private RepositoryNode _node = null;
|
||||
private object _view = null;
|
||||
}
|
||||
}
|
64
src/ViewModels/Merge.cs
Normal file
64
src/ViewModels/Merge.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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) {
|
||||
Name = n;
|
||||
Desc = d;
|
||||
Arg = a;
|
||||
}
|
||||
}
|
||||
|
||||
public class Merge : Popup {
|
||||
public string Source {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string Into {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public List<MergeMode> Modes {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public MergeMode SelectedMode {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Merge(Repository repo, string source, string into) {
|
||||
_repo = repo;
|
||||
Source = source;
|
||||
Into = into;
|
||||
Modes = new List<MergeMode>() {
|
||||
new MergeMode("Default", "Fast-forward if possible", ""),
|
||||
new MergeMode("No Fast-forward", "Always create a merge commit", "--no-ff"),
|
||||
new MergeMode("Squash", "Use '--squash'", "--squash"),
|
||||
new MergeMode("Don't commit", "Merge without commit", "--no-commit"),
|
||||
};
|
||||
SelectedMode = Modes[0];
|
||||
View = new Views.Merge() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Merging '{Source}' into '{Into}' ...");
|
||||
var succ = new Commands.Merge(_repo.FullPath, Source, SelectedMode.Arg, SetProgressDescription).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
51
src/ViewModels/Popup.cs
Normal file
51
src/ViewModels/Popup.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Popup : ObservableValidator {
|
||||
public string HostPageId {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public object View {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool InProgress {
|
||||
get => _inProgress;
|
||||
set => SetProperty(ref _inProgress, value);
|
||||
}
|
||||
|
||||
public string ProgressDescription {
|
||||
get => _progressDescription;
|
||||
set => SetProperty(ref _progressDescription, value);
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode")]
|
||||
public bool Check() {
|
||||
if (HasErrors) return false;
|
||||
ValidateAllProperties();
|
||||
return !HasErrors;
|
||||
}
|
||||
|
||||
public virtual Task<bool> Sure() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void CallUIThread(Action action) {
|
||||
Dispatcher.UIThread.Invoke(action);
|
||||
}
|
||||
|
||||
protected void SetProgressDescription(string description) {
|
||||
CallUIThread(() => ProgressDescription = description);
|
||||
}
|
||||
|
||||
private bool _inProgress = false;
|
||||
private string _progressDescription = string.Empty;
|
||||
}
|
||||
}
|
75
src/ViewModels/PopupHost.cs
Normal file
75
src/ViewModels/PopupHost.cs
Normal file
|
@ -0,0 +1,75 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class PopupHost : ObservableObject {
|
||||
public static PopupHost Active {
|
||||
get;
|
||||
set;
|
||||
} = null;
|
||||
|
||||
public Popup Popup {
|
||||
get => _popup;
|
||||
set => SetProperty(ref _popup, value);
|
||||
}
|
||||
|
||||
public static bool CanCreatePopup() {
|
||||
return Active != null && (Active._popup == null || !Active._popup.InProgress);
|
||||
}
|
||||
|
||||
public static void ShowPopup(Popup popup) {
|
||||
popup.HostPageId = Active.GetId();
|
||||
Active.Popup = popup;
|
||||
}
|
||||
|
||||
public static async void ShowAndStartPopup(Popup popup) {
|
||||
popup.HostPageId = Active.GetId();
|
||||
Active.Popup = popup;
|
||||
|
||||
if (!popup.Check()) return;
|
||||
|
||||
popup.InProgress = true;
|
||||
var task = popup.Sure();
|
||||
if (task != null) {
|
||||
var finished = await task;
|
||||
if (finished) {
|
||||
Active.Popup = null;
|
||||
} else {
|
||||
popup.InProgress = false;
|
||||
}
|
||||
} else {
|
||||
Active.Popup = null;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string GetId() {
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async void ProcessPopup() {
|
||||
if (_popup != null) {
|
||||
if (!_popup.Check()) return;
|
||||
|
||||
_popup.InProgress = true;
|
||||
var task = _popup.Sure();
|
||||
if (task != null) {
|
||||
var finished = await task;
|
||||
if (finished) {
|
||||
Popup = null;
|
||||
} else {
|
||||
_popup.InProgress = false;
|
||||
}
|
||||
} else {
|
||||
Popup = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelPopup() {
|
||||
if (_popup == null) return;
|
||||
if (_popup.InProgress) return;
|
||||
Popup = null;
|
||||
}
|
||||
|
||||
private Popup _popup = null;
|
||||
}
|
||||
}
|
258
src/ViewModels/Preference.cs
Normal file
258
src/ViewModels/Preference.cs
Normal file
|
@ -0,0 +1,258 @@
|
|||
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 {
|
||||
[JsonIgnore]
|
||||
public static Preference Instance {
|
||||
get {
|
||||
if (_instance == null) {
|
||||
if (!File.Exists(_savePath)) {
|
||||
_instance = new Preference();
|
||||
} else {
|
||||
try {
|
||||
_instance = JsonSerializer.Deserialize(File.ReadAllText(_savePath), JsonSerializationCodeGen.Default.Preference);
|
||||
} catch {
|
||||
_instance = new Preference();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_instance.Repositories.RemoveAll(x => !Directory.Exists(x.FullPath));
|
||||
|
||||
if (!_instance.IsGitConfigured) {
|
||||
_instance.GitInstallDir = Native.OS.FindGitInstallDir();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public string Locale {
|
||||
get => _locale;
|
||||
set {
|
||||
if (SetProperty(ref _locale, value)) {
|
||||
App.SetLocale(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Theme {
|
||||
get => _theme;
|
||||
set {
|
||||
if (SetProperty(ref _theme, value)) {
|
||||
App.SetTheme(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MaxHistoryCommits {
|
||||
get => _maxHistoryCommits;
|
||||
set => SetProperty(ref _maxHistoryCommits, value);
|
||||
}
|
||||
|
||||
public bool RestoreTabs {
|
||||
get => _restoreTabs;
|
||||
set => SetProperty(ref _restoreTabs, value);
|
||||
}
|
||||
|
||||
public bool UseMacOSStyle {
|
||||
get => _useMacOSStyle;
|
||||
set => SetProperty(ref _useMacOSStyle, value);
|
||||
}
|
||||
|
||||
public bool UseTwoColumnsLayoutInHistories {
|
||||
get => _useTwoColumnsLayoutInHistories;
|
||||
set => SetProperty(ref _useTwoColumnsLayoutInHistories, value);
|
||||
}
|
||||
|
||||
public bool UseCombinedTextDiff {
|
||||
get => _useCombinedTextDiff;
|
||||
set => SetProperty(ref _useCombinedTextDiff, value);
|
||||
}
|
||||
|
||||
public Models.ChangeViewMode UnstagedChangeViewMode {
|
||||
get => _unstagedChangeViewMode;
|
||||
set => SetProperty(ref _unstagedChangeViewMode, value);
|
||||
}
|
||||
|
||||
public Models.ChangeViewMode StagedChangeViewMode {
|
||||
get => _stagedChangeViewMode;
|
||||
set => SetProperty(ref _stagedChangeViewMode, value);
|
||||
}
|
||||
|
||||
public Models.ChangeViewMode CommitChangeViewMode {
|
||||
get => _commitChangeViewMode;
|
||||
set => SetProperty(ref _commitChangeViewMode, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsGitConfigured {
|
||||
get => !string.IsNullOrEmpty(GitInstallDir) && Directory.Exists(GitInstallDir);
|
||||
}
|
||||
|
||||
public string GitInstallDir {
|
||||
get => Native.OS.GitInstallDir;
|
||||
set {
|
||||
if (Native.OS.GitInstallDir != value) {
|
||||
Native.OS.GitInstallDir = value;
|
||||
OnPropertyChanged(nameof(GitInstallDir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GitDefaultCloneDir {
|
||||
get => _gitDefaultCloneDir;
|
||||
set => SetProperty(ref _gitDefaultCloneDir, value);
|
||||
}
|
||||
|
||||
public bool GitAutoFetch {
|
||||
get => _gitAutoFetch;
|
||||
set => SetProperty(ref _gitAutoFetch, value);
|
||||
}
|
||||
|
||||
public int ExternalMergeToolType {
|
||||
get => _externalMergeToolType;
|
||||
set => SetProperty(ref _externalMergeToolType, value);
|
||||
}
|
||||
|
||||
public string ExternalMergeToolPath {
|
||||
get => _externalMergeToolPath;
|
||||
set => SetProperty(ref _externalMergeToolPath, value);
|
||||
}
|
||||
|
||||
public string ExternalMergeToolCmd {
|
||||
get => _externalMergeToolCmd;
|
||||
set => SetProperty(ref _externalMergeToolCmd, value);
|
||||
}
|
||||
|
||||
public string ExternalMergeToolDiffCmd {
|
||||
get => _externalMergeToolDiffCmd;
|
||||
set => SetProperty(ref _externalMergeToolDiffCmd, value);
|
||||
}
|
||||
|
||||
public List<Repository> Repositories {
|
||||
get;
|
||||
set;
|
||||
} = new List<Repository>();
|
||||
|
||||
public AvaloniaList<RepositoryNode> RepositoryNodes {
|
||||
get => _repositoryNodes;
|
||||
set => SetProperty(ref _repositoryNodes, value);
|
||||
}
|
||||
|
||||
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) {
|
||||
return l.IsRepository ? 1 : -1;
|
||||
} else {
|
||||
return l.Name.CompareTo(r.Name);
|
||||
}
|
||||
});
|
||||
|
||||
collection.Clear();
|
||||
foreach (var one in list) {
|
||||
collection.Add(one);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
RemoveNode(node);
|
||||
AddNode(node, to);
|
||||
}
|
||||
|
||||
public static void RemoveNode(RepositoryNode node) {
|
||||
RemoveNodeRecursive(node, _instance._repositoryNodes);
|
||||
}
|
||||
|
||||
public static Repository FindRepository(string path) {
|
||||
var dir = new DirectoryInfo(path);
|
||||
foreach (var repo in _instance.Repositories) {
|
||||
if (repo.FullPath == dir.FullName) return repo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Repository AddRepository(string rootDir, string gitDir) {
|
||||
var repo = FindRepository(rootDir);
|
||||
if (repo != null) {
|
||||
repo.GitDir = gitDir;
|
||||
return repo;
|
||||
}
|
||||
|
||||
var dir = new DirectoryInfo(rootDir);
|
||||
repo = new Repository() {
|
||||
FullPath = dir.FullName,
|
||||
GitDir = gitDir
|
||||
};
|
||||
|
||||
_instance.Repositories.Add(repo);
|
||||
return repo;
|
||||
}
|
||||
|
||||
public static void Save() {
|
||||
var dir = Path.GetDirectoryName(_savePath);
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
|
||||
var data = JsonSerializer.Serialize(_instance, JsonSerializationCodeGen.Default.Preference);
|
||||
File.WriteAllText(_savePath, data);
|
||||
}
|
||||
|
||||
private static bool RemoveNodeRecursive(RepositoryNode node, AvaloniaList<RepositoryNode> collection) {
|
||||
if (collection.Contains(node)) {
|
||||
collection.Remove(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (RepositoryNode one in collection) {
|
||||
if (RemoveNodeRecursive(node, one.SubNodes)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Preference _instance = null;
|
||||
private static readonly string _savePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"SourceGit",
|
||||
"preference.json");
|
||||
|
||||
private string _locale = "en_US";
|
||||
private string _theme = "Default";
|
||||
private int _maxHistoryCommits = 20000;
|
||||
private bool _restoreTabs = false;
|
||||
private bool _useMacOSStyle = OperatingSystem.IsMacOS();
|
||||
private bool _useTwoColumnsLayoutInHistories = false;
|
||||
private bool _useCombinedTextDiff = true;
|
||||
|
||||
private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List;
|
||||
private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List;
|
||||
private Models.ChangeViewMode _commitChangeViewMode = Models.ChangeViewMode.List;
|
||||
|
||||
private string _gitDefaultCloneDir = string.Empty;
|
||||
private bool _gitAutoFetch = false;
|
||||
|
||||
private int _externalMergeToolType = 0;
|
||||
private string _externalMergeToolPath = string.Empty;
|
||||
private string _externalMergeToolCmd = string.Empty;
|
||||
private string _externalMergeToolDiffCmd = string.Empty;
|
||||
|
||||
private AvaloniaList<RepositoryNode> _repositoryNodes = new AvaloniaList<RepositoryNode>();
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true)]
|
||||
[JsonSerializable(typeof(Preference))]
|
||||
internal partial class JsonSerializationCodeGen : JsonSerializerContext { }
|
||||
}
|
28
src/ViewModels/PruneRemote.cs
Normal file
28
src/ViewModels/PruneRemote.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class PruneRemote : Popup {
|
||||
public Models.Remote Remote {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public PruneRemote(Repository repo, Models.Remote remote) {
|
||||
_repo = repo;
|
||||
Remote = remote;
|
||||
View = new Views.PruneRemote() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription("Run `prune` on remote ...");
|
||||
var succ = new Commands.Remote(_repo.FullPath).Prune(Remote.Name);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
129
src/ViewModels/Pull.cs
Normal file
129
src/ViewModels/Pull.cs
Normal file
|
@ -0,0 +1,129 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Pull : Popup {
|
||||
public List<Models.Remote> Remotes => _repo.Remotes;
|
||||
public Models.Branch Current => _current;
|
||||
|
||||
public bool HasSpecifiedRemoteBranch {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Models.Remote SelectedRemote {
|
||||
get => _selectedRemote;
|
||||
set {
|
||||
if (SetProperty(ref _selectedRemote, value)) {
|
||||
var branches = new List<Models.Branch>();
|
||||
foreach (var branch in _repo.Branches) {
|
||||
if (branch.Remote == value.Name) branches.Add(branch);
|
||||
}
|
||||
RemoteBranches = branches;
|
||||
SelectedBranch = branches.Count > 0 ? branches[0] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
get => _selectedBranch;
|
||||
set => SetProperty(ref _selectedBranch, value);
|
||||
}
|
||||
|
||||
public bool UseRebase {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool AutoStash {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Pull(Repository repo, Models.Branch specifiedRemoteBranch) {
|
||||
_repo = repo;
|
||||
_current = repo.Branches.Find(x => x.IsCurrent);
|
||||
|
||||
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) {
|
||||
_selectedRemote = repo.Remotes.Find(x => x.Name == branch.Remote);
|
||||
_selectedBranch = branch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HasSpecifiedRemoteBranch = false;
|
||||
}
|
||||
|
||||
// Make sure remote is exists.
|
||||
if (_selectedRemote == null) {
|
||||
_selectedRemote = repo.Remotes[0];
|
||||
_selectedBranch = null;
|
||||
HasSpecifiedRemoteBranch = false;
|
||||
}
|
||||
|
||||
_remoteBranches = new List<Models.Branch>();
|
||||
foreach (var branch in _repo.Branches) {
|
||||
if (branch.Remote == _selectedRemote.Name) _remoteBranches.Add(branch);
|
||||
}
|
||||
|
||||
if (_selectedBranch == null && _remoteBranches.Count > 0) {
|
||||
_selectedBranch = _remoteBranches[0];
|
||||
}
|
||||
|
||||
View = new Views.Pull() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
var needPopStash = false;
|
||||
if (AutoStash && _repo.WorkingCopyChangesCount > 0) {
|
||||
SetProgressDescription("Adding untracked changes...");
|
||||
var succ = new Commands.Add(_repo.FullPath).Exec();
|
||||
if (succ) {
|
||||
SetProgressDescription("Stash local changes...");
|
||||
succ = new Commands.Stash(_repo.FullPath).Push("PULL_AUTO_STASH");
|
||||
}
|
||||
|
||||
if (!succ) {
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return false;
|
||||
}
|
||||
|
||||
needPopStash = true;
|
||||
}
|
||||
|
||||
SetProgressDescription($"Pull {_selectedRemote.Name}/{_selectedBranch.Name}...");
|
||||
var rs = new Commands.Pull(_repo.FullPath, _selectedRemote.Name, _selectedBranch.Name, UseRebase, SetProgressDescription).Exec();
|
||||
if (rs && needPopStash) {
|
||||
SetProgressDescription("Re-apply local changes...");
|
||||
rs = new Commands.Stash(_repo.FullPath).Pop("stash@{0}");
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return rs;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private Models.Branch _current = null;
|
||||
private Models.Remote _selectedRemote = null;
|
||||
private List<Models.Branch> _remoteBranches = null;
|
||||
private Models.Branch _selectedBranch = null;
|
||||
}
|
||||
}
|
174
src/ViewModels/Push.cs
Normal file
174
src/ViewModels/Push.cs
Normal file
|
@ -0,0 +1,174 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Push : Popup {
|
||||
public bool HasSpecifiedLocalBranch {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Local branch is required!!!")]
|
||||
public Models.Branch SelectedLocalBranch {
|
||||
get => _selectedLocalBranch;
|
||||
set {
|
||||
if (SetProperty(ref _selectedLocalBranch, value)) {
|
||||
// If selected local branch has upstream branch. Try to find it's remote.
|
||||
if (!string.IsNullOrEmpty(value.Upstream)) {
|
||||
var branch = _repo.Branches.Find(x => x.FullName == value.Upstream);
|
||||
if (branch != null) {
|
||||
var remote = _repo.Remotes.Find(x => x.Name == branch.Remote);
|
||||
if (remote != null && remote != _selectedRemote) {
|
||||
SelectedRemote = remote;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-generate remote branches and auto-select remote branches.
|
||||
AutoSelectBranchByRemote();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Models.Branch> LocalBranches {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public List<Models.Remote> Remotes {
|
||||
get => _repo.Remotes;
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Remote is required!!!")]
|
||||
public Models.Remote SelectedRemote {
|
||||
get => _selectedRemote;
|
||||
set {
|
||||
if (SetProperty(ref _selectedRemote, value)) AutoSelectBranchByRemote();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Models.Branch> RemoteBranches {
|
||||
get => _remoteBranches;
|
||||
private set => SetProperty(ref _remoteBranches, value);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Remote branch is required!!!")]
|
||||
public Models.Branch SelectedRemoteBranch {
|
||||
get => _selectedRemoteBranch;
|
||||
set => SetProperty(ref _selectedRemoteBranch, value);
|
||||
}
|
||||
|
||||
public bool PushAllTags {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool ForcePush {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (branch.IsLocal) LocalBranches.Add(branch);
|
||||
if (branch.IsCurrent) current = branch;
|
||||
}
|
||||
|
||||
// Set default selected local branch.
|
||||
if (localBranch != null) {
|
||||
_selectedLocalBranch = localBranch;
|
||||
HasSpecifiedLocalBranch = true;
|
||||
} 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) {
|
||||
_selectedRemote = repo.Remotes.Find(x => x.Name == branch.Remote);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set default remote to the first if haven't been set.
|
||||
if (_selectedRemote == null) _selectedRemote = repo.Remotes[0];
|
||||
|
||||
// Auto select preferred remote branch.
|
||||
AutoSelectBranchByRemote();
|
||||
|
||||
View = new Views.Push() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
var remoteBranchName = _selectedRemoteBranch.Name.Replace(" (new)", "");
|
||||
SetProgressDescription($"Push {_selectedLocalBranch.Name} -> {_selectedRemote.Name}/{remoteBranchName} ...");
|
||||
var succ = new Commands.Push(
|
||||
_repo.FullPath,
|
||||
_selectedLocalBranch.Name,
|
||||
_selectedRemote.Name,
|
||||
remoteBranchName,
|
||||
PushAllTags,
|
||||
ForcePush,
|
||||
string.IsNullOrEmpty(_selectedLocalBranch.Upstream),
|
||||
SetProgressDescription).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private void AutoSelectBranchByRemote() {
|
||||
// Gather branches.
|
||||
var branches = new List<Models.Branch>();
|
||||
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) {
|
||||
RemoteBranches = branches;
|
||||
SelectedRemoteBranch = branch;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find best remote branch by name.
|
||||
foreach (var branch in branches) {
|
||||
if (_selectedLocalBranch.Name == branch.Name) {
|
||||
RemoteBranches = branches;
|
||||
SelectedRemoteBranch = branch;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a fake new branch.
|
||||
var fake = new Models.Branch() {
|
||||
Name = $"{_selectedLocalBranch.Name} (new)",
|
||||
Remote = _selectedRemote.Name,
|
||||
};
|
||||
branches.Add(fake);
|
||||
RemoteBranches = branches;
|
||||
SelectedRemoteBranch = fake;
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
39
src/ViewModels/PushTag.cs
Normal file
39
src/ViewModels/PushTag.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class PushTag : Popup {
|
||||
public Models.Tag Target {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public List<Models.Remote> Remotes {
|
||||
get => _repo.Remotes;
|
||||
}
|
||||
|
||||
public Models.Remote SelectedRemote {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
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() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Pushing tag '{Target.Name}' to remote '{SelectedRemote.Name}' ...");
|
||||
var succ = new Commands.Push(_repo.FullPath, SelectedRemote.Name, Target.Name, false).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
50
src/ViewModels/Rebase.cs
Normal file
50
src/ViewModels/Rebase.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Rebase : Popup {
|
||||
public Models.Branch Current {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public object On {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool AutoStash {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Rebase(Repository repo, Models.Branch current, Models.Branch on) {
|
||||
_repo = repo;
|
||||
_revision = on.Head;
|
||||
Current = current;
|
||||
On = on;
|
||||
AutoStash = true;
|
||||
View = new Views.Rebase() { DataContext = this };
|
||||
}
|
||||
|
||||
public Rebase(Repository repo, Models.Branch current, Models.Commit on) {
|
||||
_repo = repo;
|
||||
_revision = on.SHA;
|
||||
Current = current;
|
||||
On = on;
|
||||
AutoStash = true;
|
||||
View = new Views.Rebase() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
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;
|
||||
}
|
||||
}
|
53
src/ViewModels/RenameBranch.cs
Normal file
53
src/ViewModels/RenameBranch.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class RenameBranch : Popup {
|
||||
public Models.Branch Target {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Branch name is required!!!")]
|
||||
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
|
||||
[CustomValidation(typeof(RenameBranch), nameof(ValidateBranchName))]
|
||||
public string Name {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value, true);
|
||||
}
|
||||
|
||||
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) {
|
||||
return new ValidationResult("A branch with same name already exists!!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
if (_name == Target.Name) return null;
|
||||
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Rename '{Target.Name}'");
|
||||
var succ = Commands.Branch.Rename(_repo.FullPath, Target.Name, _name);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _name = string.Empty;
|
||||
}
|
||||
}
|
1082
src/ViewModels/Repository.cs
Normal file
1082
src/ViewModels/Repository.cs
Normal file
File diff suppressed because it is too large
Load diff
69
src/ViewModels/RepositoryConfigure.cs
Normal file
69
src/ViewModels/RepositoryConfigure.cs
Normal file
|
@ -0,0 +1,69 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class RepositoryConfigure : Popup {
|
||||
public string UserName {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string UserEmail {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool GPGSigningEnabled {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string GPGUserSigningKey {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string HttpProxy {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public RepositoryConfigure(Repository repo) {
|
||||
_repo = repo;
|
||||
|
||||
_cached = new Commands.Config(repo.FullPath).ListAll();
|
||||
if (_cached.ContainsKey("user.name")) UserName = _cached["user.name"];
|
||||
if (_cached.ContainsKey("user.email")) UserEmail = _cached["user.email"];
|
||||
if (_cached.ContainsKey("commit.gpgsign")) GPGSigningEnabled = _cached["commit.gpgsign"] == "true";
|
||||
if (_cached.ContainsKey("user.signingkey")) GPGUserSigningKey = _cached["user.signingkey"];
|
||||
if (_cached.ContainsKey("http.proxy")) HttpProxy = _cached["user.signingkey"];
|
||||
|
||||
View = new Views.RepositoryConfigure() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
SetIfChanged("user.name", UserName);
|
||||
SetIfChanged("user.email", UserEmail);
|
||||
SetIfChanged("commit.gpgsign", GPGSigningEnabled ? "true" : "false");
|
||||
SetIfChanged("user.signingkey", GPGUserSigningKey);
|
||||
SetIfChanged("http.proxy", HttpProxy);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SetIfChanged(string key, string value) {
|
||||
bool changed = false;
|
||||
if (_cached.ContainsKey(key)) {
|
||||
changed = value != _cached[key];
|
||||
} else if (!string.IsNullOrEmpty(value)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
new Commands.Config(_repo.FullPath).Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private Dictionary<string, string> _cached = null;
|
||||
}
|
||||
}
|
73
src/ViewModels/RepositoryNode.cs
Normal file
73
src/ViewModels/RepositoryNode.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class RepositoryNode : ObservableObject {
|
||||
public string Id {
|
||||
get => _id;
|
||||
set => SetProperty(ref _id, value);
|
||||
}
|
||||
|
||||
public string Name {
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
public int Bookmark {
|
||||
get => _bookmark;
|
||||
set => SetProperty(ref _bookmark, value);
|
||||
}
|
||||
|
||||
public bool IsRepository {
|
||||
get => _isRepository;
|
||||
set => SetProperty(ref _isRepository, value);
|
||||
}
|
||||
|
||||
public bool IsExpanded {
|
||||
get => _isExpanded;
|
||||
set => SetProperty(ref _isExpanded, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsVisible {
|
||||
get => _isVisible;
|
||||
set => SetProperty(ref _isVisible, value);
|
||||
}
|
||||
|
||||
public AvaloniaList<RepositoryNode> SubNodes {
|
||||
get => _subNodes;
|
||||
set => SetProperty(ref _subNodes, value);
|
||||
}
|
||||
|
||||
public void Edit() {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new EditRepositoryNode(this));
|
||||
}
|
||||
|
||||
public void AddSubFolder() {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateGroup(this));
|
||||
}
|
||||
|
||||
public void OpenInFileManager() {
|
||||
if (!IsRepository) return;
|
||||
Native.OS.OpenInFileManager(_id);
|
||||
}
|
||||
|
||||
public void OpenTerminal() {
|
||||
if (!IsRepository) return;
|
||||
Native.OS.OpenTerminal(_id);
|
||||
}
|
||||
|
||||
public void Delete() {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new DeleteRepositoryNode(this));
|
||||
}
|
||||
|
||||
private string _id = string.Empty;
|
||||
private string _name = string.Empty;
|
||||
private bool _isRepository = false;
|
||||
private int _bookmark = 0;
|
||||
private bool _isExpanded = false;
|
||||
private bool _isVisible = true;
|
||||
private AvaloniaList<RepositoryNode> _subNodes = new AvaloniaList<RepositoryNode>();
|
||||
}
|
||||
}
|
66
src/ViewModels/Reset.cs
Normal file
66
src/ViewModels/Reset.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using Avalonia.Media;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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) {
|
||||
Name = n;
|
||||
Desc = d;
|
||||
Arg = a;
|
||||
Color = b;
|
||||
}
|
||||
}
|
||||
|
||||
public class Reset : Popup {
|
||||
public Models.Branch Current {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Models.Commit To {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public List<ResetMode> Modes {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public ResetMode SelectedMode {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Reset(Repository repo, Models.Branch current, Models.Commit to) {
|
||||
_repo = repo;
|
||||
Current = current;
|
||||
To = to;
|
||||
Modes = new List<ResetMode>() {
|
||||
new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", Brushes.Green),
|
||||
new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", Brushes.Orange),
|
||||
new ResetMode("Hard", "Discard all changes", "--hard", Brushes.Red),
|
||||
};
|
||||
SelectedMode = Modes[0];
|
||||
View = new Views.Reset() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Reset current branch to {To.SHA} ...");
|
||||
var succ = new Commands.Reset(_repo.FullPath, To.SHA, SelectedMode.Arg).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
34
src/ViewModels/Revert.cs
Normal file
34
src/ViewModels/Revert.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Revert : Popup {
|
||||
public Models.Commit Target {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool AutoCommit {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Revert(Repository repo, Models.Commit target) {
|
||||
_repo = repo;
|
||||
Target = target;
|
||||
AutoCommit = true;
|
||||
View = new Views.Revert() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Revert commit '{Target.SHA}' ...");
|
||||
var succ = new Commands.Revert(_repo.FullPath, Target.SHA, AutoCommit).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
}
|
||||
}
|
172
src/ViewModels/RevisionCompare.cs
Normal file
172
src/ViewModels/RevisionCompare.cs
Normal file
|
@ -0,0 +1,172 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class RevisionCompare : ObservableObject {
|
||||
public Models.Commit StartPoint {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Models.Commit EndPoint {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public List<Models.Change> VisibleChanges {
|
||||
get => _visibleChanges;
|
||||
private set => SetProperty(ref _visibleChanges, value);
|
||||
}
|
||||
|
||||
public List<FileTreeNode> ChangeTree {
|
||||
get => _changeTree;
|
||||
private set => SetProperty(ref _changeTree, value);
|
||||
}
|
||||
|
||||
public Models.Change SelectedChange {
|
||||
get => _selectedChange;
|
||||
set {
|
||||
if (SetProperty(ref _selectedChange, value)) {
|
||||
if (value == null) {
|
||||
SelectedNode = null;
|
||||
DiffContext = null;
|
||||
} else {
|
||||
SelectedNode = FileTreeNode.SelectByPath(_changeTree, value.Path);
|
||||
DiffContext = new DiffContext(_repo, new Models.DiffOption(StartPoint.SHA, EndPoint.SHA, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FileTreeNode SelectedNode {
|
||||
get => _selectedNode;
|
||||
set {
|
||||
if (SetProperty(ref _selectedNode, value)) {
|
||||
if (value == null) {
|
||||
SelectedChange = null;
|
||||
} else {
|
||||
SelectedChange = value.Backend as Models.Change;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SearchFilter {
|
||||
get => _searchFilter;
|
||||
set {
|
||||
if (SetProperty(ref _searchFilter, value)) {
|
||||
RefreshVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DiffContext DiffContext {
|
||||
get => _diffContext;
|
||||
private set => SetProperty(ref _diffContext, value);
|
||||
}
|
||||
|
||||
public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endPoint) {
|
||||
_repo = repo;
|
||||
StartPoint = startPoint;
|
||||
EndPoint = endPoint;
|
||||
|
||||
Task.Run(() => {
|
||||
_changes = new Commands.CompareRevisions(_repo, startPoint.SHA, endPoint.SHA).Result();
|
||||
|
||||
var visible = _changes;
|
||||
if (!string.IsNullOrWhiteSpace(_searchFilter)) {
|
||||
visible = new List<Models.Change>();
|
||||
foreach (var c in _changes) {
|
||||
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) {
|
||||
visible.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tree = FileTreeNode.Build(visible);
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
VisibleChanges = visible;
|
||||
ChangeTree = tree;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void NavigateTo(string commitSHA) {
|
||||
var repo = Preference.FindRepository(_repo);
|
||||
if (repo != null) repo.NavigateToCommit(commitSHA);
|
||||
}
|
||||
|
||||
public void ClearSearchFilter() {
|
||||
SearchFilter = string.Empty;
|
||||
}
|
||||
|
||||
public ContextMenu CreateChangeContextMenu(Models.Change change) {
|
||||
var menu = new ContextMenu();
|
||||
|
||||
if (change.Index != Models.ChangeState.Deleted) {
|
||||
var history = new MenuItem();
|
||||
history.Header = App.Text("FileHistory");
|
||||
history.Click += (_, ev) => {
|
||||
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
|
||||
window.Show();
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var full = Path.GetFullPath(Path.Combine(_repo, change.Path));
|
||||
var explore = new MenuItem();
|
||||
explore.Header = App.Text("RevealFile");
|
||||
explore.IsEnabled = File.Exists(full);
|
||||
explore.Click += (_, ev) => {
|
||||
Native.OS.OpenInFileManager(full, true);
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(history);
|
||||
menu.Items.Add(explore);
|
||||
}
|
||||
|
||||
var copyPath = new MenuItem();
|
||||
copyPath.Header = App.Text("CopyPath");
|
||||
copyPath.Click += (_, ev) => {
|
||||
App.CopyText(change.Path);
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(copyPath);
|
||||
return menu;
|
||||
}
|
||||
|
||||
private void RefreshVisible() {
|
||||
if (_changes == null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(_searchFilter)) {
|
||||
VisibleChanges = _changes;
|
||||
} else {
|
||||
var visible = new List<Models.Change>();
|
||||
foreach (var c in _changes) {
|
||||
if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) {
|
||||
visible.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
VisibleChanges = visible;
|
||||
}
|
||||
|
||||
ChangeTree = FileTreeNode.Build(_visibleChanges);
|
||||
}
|
||||
|
||||
private string _repo = string.Empty;
|
||||
private List<Models.Change> _changes = null;
|
||||
private List<Models.Change> _visibleChanges = null;
|
||||
private List<FileTreeNode> _changeTree = null;
|
||||
private Models.Change _selectedChange = null;
|
||||
private FileTreeNode _selectedNode = null;
|
||||
private string _searchFilter = string.Empty;
|
||||
private DiffContext _diffContext = null;
|
||||
}
|
||||
}
|
39
src/ViewModels/Reword.cs
Normal file
39
src/ViewModels/Reword.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Reword : Popup {
|
||||
public Models.Commit Head {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Commit message is required!!!")]
|
||||
public string Message {
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value, true);
|
||||
}
|
||||
|
||||
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() {
|
||||
if (_message == Head.FullMessage) return null;
|
||||
|
||||
_repo.SetWatcherEnabled(false);
|
||||
return Task.Run(() => {
|
||||
SetProgressDescription($"Editing head commit message ...");
|
||||
var succ = new Commands.Commit(_repo.FullPath, _message, true, true).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _message = string.Empty;
|
||||
}
|
||||
}
|
43
src/ViewModels/Squash.cs
Normal file
43
src/ViewModels/Squash.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Squash : Popup {
|
||||
public Models.Commit Head {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Models.Commit Parent {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Commit message is required!!!")]
|
||||
public string Message {
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value, true);
|
||||
}
|
||||
|
||||
public Squash(Repository repo, Models.Commit head, Models.Commit parent) {
|
||||
_repo = repo;
|
||||
_message = parent.FullMessage;
|
||||
Head = head;
|
||||
Parent = parent;
|
||||
View = new Views.Squash() { DataContext = this };
|
||||
}
|
||||
|
||||
public override Task<bool> Sure() {
|
||||
_repo.SetWatcherEnabled(false);
|
||||
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));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private string _message = string.Empty;
|
||||
}
|
||||
}
|
55
src/ViewModels/StashChanges.cs
Normal file
55
src/ViewModels/StashChanges.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class StashChanges : Popup {
|
||||
|
||||
public string Message {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool CanIgnoreUntracked {
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool IncludeUntracked {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
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() {
|
||||
var jobs = _changes;
|
||||
if (CanIgnoreUntracked && !IncludeUntracked) {
|
||||
jobs = new List<Models.Change>();
|
||||
foreach (var job in _changes) {
|
||||
if (job.WorkTree != Models.ChangeState.Untracked && job.WorkTree != Models.ChangeState.Added) {
|
||||
jobs.Add(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jobs.Count == 0) return null;
|
||||
|
||||
_repo.SetWatcherEnabled(false);
|
||||
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;
|
||||
}
|
||||
}
|
105
src/ViewModels/StashesPage.cs
Normal file
105
src/ViewModels/StashesPage.cs
Normal file
|
@ -0,0 +1,105 @@
|
|||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class StashesPage : ObservableObject {
|
||||
public int Count {
|
||||
get => _stashes == null ? 0 : _stashes.Count;
|
||||
}
|
||||
|
||||
public List<Models.Stash> Stashes {
|
||||
get => _stashes;
|
||||
set {
|
||||
if (SetProperty(ref _stashes, value)) {
|
||||
SelectedStash = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Models.Stash SelectedStash {
|
||||
get => _selectedStash;
|
||||
set {
|
||||
if (SetProperty(ref _selectedStash, value)) {
|
||||
if (value == null) {
|
||||
Changes = null;
|
||||
} else {
|
||||
Task.Run(() => {
|
||||
var changes = new Commands.QueryStashChanges(_repo.FullPath, value.SHA).Result();
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
Changes = changes;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Models.Change> Changes {
|
||||
get => _changes;
|
||||
private set {
|
||||
if (SetProperty(ref _changes, value)) {
|
||||
SelectedChange = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Models.Change SelectedChange {
|
||||
get => _selectedChange;
|
||||
set {
|
||||
if (SetProperty(ref _selectedChange, value)) {
|
||||
if (value == null) {
|
||||
DiffContext = null;
|
||||
} else {
|
||||
DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption($"{_selectedStash.SHA}^", _selectedStash.SHA, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DiffContext DiffContext {
|
||||
get => _diffContext;
|
||||
private set => SetProperty(ref _diffContext, value);
|
||||
}
|
||||
|
||||
public StashesPage(Repository repo) {
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
new Commands.Stash(_repo.FullPath).Pop(stash.Name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
PopupHost.ShowPopup(new ClearStashes(_repo));
|
||||
}
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private List<Models.Stash> _stashes = null;
|
||||
private Models.Stash _selectedStash = null;
|
||||
private List<Models.Change> _changes = null;
|
||||
private Models.Change _selectedChange = null;
|
||||
private DiffContext _diffContext = null;
|
||||
}
|
||||
}
|
52
src/ViewModels/TwoSideTextDiff.cs
Normal file
52
src/ViewModels/TwoSideTextDiff.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using Avalonia;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class TwoSideTextDiff : ObservableObject {
|
||||
public Vector SyncScrollOffset {
|
||||
get => _syncScrollOffset;
|
||||
set => SetProperty(ref _syncScrollOffset, value);
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
FillEmptyLines();
|
||||
}
|
||||
|
||||
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) {
|
||||
int diff = Old.Count - New.Count;
|
||||
for (int i = 0; i < diff; i++) New.Add(new Models.TextDiffLine());
|
||||
}
|
||||
}
|
||||
|
||||
private Vector _syncScrollOffset;
|
||||
}
|
||||
}
|
102
src/ViewModels/Welcome.cs
Normal file
102
src/ViewModels/Welcome.cs
Normal file
|
@ -0,0 +1,102 @@
|
|||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class Welcome : ObservableObject {
|
||||
public bool IsClearSearchVisible {
|
||||
get => !string.IsNullOrEmpty(_searchFilter);
|
||||
}
|
||||
|
||||
public AvaloniaList<RepositoryNode> RepositoryNodes {
|
||||
get => Preference.Instance.RepositoryNodes;
|
||||
}
|
||||
|
||||
public string SearchFilter {
|
||||
get => _searchFilter;
|
||||
set {
|
||||
if (SetProperty(ref _searchFilter, value)) {
|
||||
Referesh();
|
||||
OnPropertyChanged(nameof(IsClearSearchVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void InitRepository(string path) {
|
||||
if (!Preference.Instance.IsGitConfigured) {
|
||||
App.RaiseException(PopupHost.Active.GetId(), App.Text("NotConfigured"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (PopupHost.CanCreatePopup()) {
|
||||
PopupHost.ShowPopup(new Init(path));
|
||||
}
|
||||
}
|
||||
|
||||
public void Clone(object param) {
|
||||
var page = param as LauncherPage;
|
||||
|
||||
if (!Preference.Instance.IsGitConfigured) {
|
||||
App.RaiseException(page.GetId(), App.Text("NotConfigured"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (PopupHost.CanCreatePopup()) {
|
||||
PopupHost.ShowPopup(new Clone(page));
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenTerminal() {
|
||||
if (!Preference.Instance.IsGitConfigured) {
|
||||
App.RaiseException(PopupHost.Active.GetId(), App.Text("NotConfigured"));
|
||||
} else {
|
||||
Native.OS.OpenTerminal(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearSearchFilter() {
|
||||
SearchFilter = string.Empty;
|
||||
}
|
||||
|
||||
public void AddFolder() {
|
||||
if (PopupHost.CanCreatePopup()) PopupHost.ShowPopup(new CreateGroup(null));
|
||||
}
|
||||
|
||||
public void MoveNode(RepositoryNode from, RepositoryNode to) {
|
||||
Preference.MoveNode(from, to);
|
||||
}
|
||||
|
||||
private void Referesh() {
|
||||
if (string.IsNullOrWhiteSpace(_searchFilter)) {
|
||||
foreach (var node in RepositoryNodes) ResetVisibility(node);
|
||||
} else {
|
||||
foreach (var node in RepositoryNodes) SetVisibilityBySearch(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)) {
|
||||
node.IsVisible = true;
|
||||
foreach (var subNode in node.SubNodes) ResetVisibility(subNode);
|
||||
} else {
|
||||
bool hasVisibleSubNode = false;
|
||||
foreach (var subNode in node.SubNodes) {
|
||||
SetVisibilityBySearch(subNode);
|
||||
hasVisibleSubNode |= subNode.IsVisible;
|
||||
}
|
||||
node.IsVisible = hasVisibleSubNode;
|
||||
}
|
||||
} else {
|
||||
node.IsVisible = node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private string _searchFilter = string.Empty;
|
||||
}
|
||||
}
|
600
src/ViewModels/WorkingCopy.cs
Normal file
600
src/ViewModels/WorkingCopy.cs
Normal file
|
@ -0,0 +1,600 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels {
|
||||
public class ConflictContext {
|
||||
public Models.Change Change { get; set; }
|
||||
}
|
||||
|
||||
public class ViewChangeDetailContext {
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public bool IsUnstaged { get; set; } = false;
|
||||
}
|
||||
|
||||
public class WorkingCopy : ObservableObject {
|
||||
public bool IsStaging {
|
||||
get => _isStaging;
|
||||
private set => SetProperty(ref _isStaging, value);
|
||||
}
|
||||
|
||||
public bool IsUnstaging {
|
||||
get => _isUnstaging;
|
||||
private set => SetProperty(ref _isUnstaging, value);
|
||||
}
|
||||
|
||||
public bool IsCommitting {
|
||||
get => _isCommitting;
|
||||
private set => SetProperty(ref _isCommitting, value);
|
||||
}
|
||||
|
||||
public bool UseAmend {
|
||||
get => _useAmend;
|
||||
set => SetProperty(ref _useAmend, value);
|
||||
}
|
||||
|
||||
public List<Models.Change> Unstaged {
|
||||
get => _unstaged;
|
||||
private set => SetProperty(ref _unstaged, value);
|
||||
}
|
||||
|
||||
public List<Models.Change> Staged {
|
||||
get => _staged;
|
||||
private set => SetProperty(ref _staged, value);
|
||||
}
|
||||
|
||||
public int Count {
|
||||
get => _count;
|
||||
}
|
||||
|
||||
public List<FileTreeNode> UnstagedTree {
|
||||
get => _unstagedTree;
|
||||
private set => SetProperty(ref _unstagedTree, value);
|
||||
}
|
||||
|
||||
public List<FileTreeNode> StagedTree {
|
||||
get => _stagedTree;
|
||||
private set => SetProperty(ref _stagedTree, value);
|
||||
}
|
||||
|
||||
public object DetailContext {
|
||||
get => _detailContext;
|
||||
private set => SetProperty(ref _detailContext, value);
|
||||
}
|
||||
|
||||
public string CommitMessage {
|
||||
get => _commitMessage;
|
||||
set => SetProperty(ref _commitMessage, value);
|
||||
}
|
||||
|
||||
public WorkingCopy(Repository repo) {
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public bool SetData(List<Models.Change> changes) {
|
||||
var unstaged = new List<Models.Change>();
|
||||
var staged = new List<Models.Change>();
|
||||
|
||||
var viewFile = _lastViewChange == null ? string.Empty : _lastViewChange.FilePath;
|
||||
var viewChange = null as Models.Change;
|
||||
var hasConflict = false;
|
||||
foreach (var c in changes) {
|
||||
if (c.Path == viewFile) {
|
||||
viewChange = c;
|
||||
}
|
||||
|
||||
if (c.Index == Models.ChangeState.Modified
|
||||
|| c.Index == Models.ChangeState.Added
|
||||
|| c.Index == Models.ChangeState.Deleted
|
||||
|| c.Index == Models.ChangeState.Renamed) {
|
||||
staged.Add(c);
|
||||
}
|
||||
|
||||
if (c.WorkTree != Models.ChangeState.None) {
|
||||
unstaged.Add(c);
|
||||
hasConflict |= c.IsConflit;
|
||||
}
|
||||
}
|
||||
|
||||
_count = changes.Count;
|
||||
|
||||
var unstagedTree = FileTreeNode.Build(unstaged);
|
||||
var stagedTree = FileTreeNode.Build(staged);
|
||||
Dispatcher.UIThread.Invoke(() => {
|
||||
_isLoadingData = true;
|
||||
Unstaged = unstaged;
|
||||
Staged = staged;
|
||||
UnstagedTree = unstagedTree;
|
||||
StagedTree = stagedTree;
|
||||
_isLoadingData = false;
|
||||
SetDetail(viewChange, _lastViewChange == null || _lastViewChange.IsUnstaged);
|
||||
});
|
||||
|
||||
return hasConflict;
|
||||
}
|
||||
|
||||
public void SetDetail(Models.Change change, bool isUnstaged) {
|
||||
if (_isLoadingData) return;
|
||||
|
||||
if (change == null) {
|
||||
_lastViewChange = null;
|
||||
DetailContext = null;
|
||||
} else if (change.IsConflit) {
|
||||
_lastViewChange = new ViewChangeDetailContext() { FilePath = change.Path, IsUnstaged = isUnstaged };
|
||||
DetailContext = new ConflictContext() { Change = change };
|
||||
} else {
|
||||
_lastViewChange = new ViewChangeDetailContext() { FilePath = change.Path, IsUnstaged = isUnstaged };
|
||||
DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged));
|
||||
}
|
||||
}
|
||||
|
||||
public async void StageChanges(List<Models.Change> changes) {
|
||||
if (_unstaged.Count == 0 || changes.Count == 0) return;
|
||||
|
||||
IsStaging = true;
|
||||
_repo.SetWatcherEnabled(false);
|
||||
if (changes.Count == _unstaged.Count) {
|
||||
await Task.Run(() => new Commands.Add(_repo.FullPath).Exec());
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
_repo.RefreshWorkingCopyChanges();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
IsStaging = false;
|
||||
}
|
||||
|
||||
public async void UnstageChanges(List<Models.Change> changes) {
|
||||
if (_staged.Count == 0 || changes.Count == 0) return;
|
||||
|
||||
IsUnstaging = true;
|
||||
_repo.SetWatcherEnabled(false);
|
||||
if (changes.Count == _staged.Count) {
|
||||
await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
_repo.RefreshWorkingCopyChanges();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
IsUnstaging = false;
|
||||
}
|
||||
|
||||
public void Discard(List<Models.Change> changes) {
|
||||
if (PopupHost.CanCreatePopup()) {
|
||||
if (changes.Count == _count) {
|
||||
PopupHost.ShowPopup(new Discard(_repo));
|
||||
} else {
|
||||
PopupHost.ShowPopup(new Discard(_repo, changes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
await Task.Run(() => new Commands.Add(_repo.FullPath, [ctx.Change]).Exec());
|
||||
}
|
||||
_repo.RefreshWorkingCopyChanges();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
await Task.Run(() => new Commands.Add(_repo.FullPath, [ctx.Change]).Exec());
|
||||
}
|
||||
_repo.RefreshWorkingCopyChanges();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
App.RaiseException(_repo.FullPath, "Invalid merge tool in preference setting!");
|
||||
return;
|
||||
}
|
||||
|
||||
var args = tool.Type != 0 ? tool.Cmd : Preference.Instance.ExternalMergeToolCmd;
|
||||
|
||||
_repo.SetWatcherEnabled(false);
|
||||
await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, exec, args, ctx.Change.Path));
|
||||
_repo.SetWatcherEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async void DoCommit(bool autoPush) {
|
||||
if (!PopupHost.CanCreatePopup()) {
|
||||
App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_staged.Count == 0) {
|
||||
App.RaiseException(_repo.FullPath, "No files added to commit!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_commitMessage)) {
|
||||
App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!");
|
||||
return;
|
||||
}
|
||||
|
||||
PushCommitMessage();
|
||||
|
||||
IsCommitting = true;
|
||||
_repo.SetWatcherEnabled(false);
|
||||
var succ = await Task.Run(() => new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec());
|
||||
if (succ) {
|
||||
CommitMessage = string.Empty;
|
||||
UseAmend = false;
|
||||
|
||||
if (autoPush) {
|
||||
PopupHost.ShowAndStartPopup(new Push(_repo, null));
|
||||
}
|
||||
}
|
||||
_repo.RefreshWorkingCopyChanges();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
IsCommitting = false;
|
||||
}
|
||||
|
||||
public ContextMenu CreateContextMenuForUnstagedChanges(List<Models.Change> changes) {
|
||||
if (changes.Count == 0) return null;
|
||||
|
||||
var menu = new ContextMenu();
|
||||
if (changes.Count == 1) {
|
||||
var change = changes[0];
|
||||
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
|
||||
|
||||
var explore = new MenuItem();
|
||||
explore.Header = App.Text("RevealFile");
|
||||
explore.Icon = CreateMenuIcon("Icons.Folder.Open");
|
||||
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
|
||||
explore.Click += (_, e) => {
|
||||
Native.OS.OpenInFileManager(path, true);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var openWith = new MenuItem();
|
||||
openWith.Header = App.Text("OpenWith");
|
||||
openWith.Icon = CreateMenuIcon("Icons.OpenWith");
|
||||
openWith.IsEnabled = File.Exists(path);
|
||||
openWith.Click += (_, e) => {
|
||||
Native.OS.OpenWithDefaultEditor(path);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stage = new MenuItem();
|
||||
stage.Header = App.Text("FileCM.Stage");
|
||||
stage.Icon = CreateMenuIcon("Icons.File.Add");
|
||||
stage.Click += (_, e) => {
|
||||
StageChanges(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var discard = new MenuItem();
|
||||
discard.Header = App.Text("FileCM.Discard");
|
||||
discard.Icon = CreateMenuIcon("Icons.Undo");
|
||||
discard.Click += (_, e) => {
|
||||
Discard(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.Stash");
|
||||
stash.Icon = CreateMenuIcon("Icons.Stashes");
|
||||
stash.Click += (_, e) => {
|
||||
if (PopupHost.CanCreatePopup()) {
|
||||
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
|
||||
}
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var patch = new MenuItem();
|
||||
patch.Header = App.Text("FileCM.SaveAsPatch");
|
||||
patch.Icon = CreateMenuIcon("Icons.Diff");
|
||||
patch.Click += async (_, e) => {
|
||||
var topLevel = App.GetTopLevel();
|
||||
if (topLevel == null) return;
|
||||
|
||||
var options = new FilePickerSaveOptions();
|
||||
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) {
|
||||
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath));
|
||||
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var history = new MenuItem();
|
||||
history.Header = App.Text("FileHistory");
|
||||
history.Icon = CreateMenuIcon("Icons.Histories");
|
||||
history.Click += (_, e) => {
|
||||
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo.FullPath, change.Path) };
|
||||
window.Show();
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var assumeUnchanged = new MenuItem();
|
||||
assumeUnchanged.Header = App.Text("FileCM.AssumeUnchanged");
|
||||
assumeUnchanged.Icon = CreateMenuIcon("Icons.File.Ignore");
|
||||
assumeUnchanged.IsEnabled = change.WorkTree != Models.ChangeState.Untracked;
|
||||
assumeUnchanged.Click += (_, e) => {
|
||||
new Commands.AssumeUnchanged(_repo.FullPath).Add(change.Path);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var copy = new MenuItem();
|
||||
copy.Header = App.Text("CopyPath");
|
||||
copy.Icon = CreateMenuIcon("Icons.Copy");
|
||||
copy.Click += (_, e) => {
|
||||
App.CopyText(change.Path);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(explore);
|
||||
menu.Items.Add(openWith);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
menu.Items.Add(stage);
|
||||
menu.Items.Add(discard);
|
||||
menu.Items.Add(stash);
|
||||
menu.Items.Add(patch);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
menu.Items.Add(history);
|
||||
menu.Items.Add(assumeUnchanged);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
menu.Items.Add(copy);
|
||||
} else {
|
||||
var stage = new MenuItem();
|
||||
stage.Header = App.Text("FileCM.StageMulti", changes.Count);
|
||||
stage.Icon = CreateMenuIcon("Icons.File.Add");
|
||||
stage.Click += (_, e) => {
|
||||
StageChanges(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var discard = new MenuItem();
|
||||
discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
|
||||
discard.Icon = CreateMenuIcon("Icons.Undo");
|
||||
discard.Click += (_, e) => {
|
||||
Discard(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
|
||||
stash.Icon = CreateMenuIcon("Icons.Stashes");
|
||||
stash.Click += (_, e) => {
|
||||
if (PopupHost.CanCreatePopup()) {
|
||||
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
|
||||
}
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var patch = new MenuItem();
|
||||
patch.Header = App.Text("FileCM.SaveAsPatch");
|
||||
patch.Icon = CreateMenuIcon("Icons.Diff");
|
||||
patch.Click += async (o, e) => {
|
||||
var topLevel = App.GetTopLevel();
|
||||
if (topLevel == null) return;
|
||||
|
||||
var options = new FilePickerSaveOptions();
|
||||
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) {
|
||||
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, true, storageFile.Path.LocalPath));
|
||||
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(stage);
|
||||
menu.Items.Add(discard);
|
||||
menu.Items.Add(stash);
|
||||
menu.Items.Add(patch);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
public ContextMenu CreateContextMenuForStagedChanges(List<Models.Change> changes) {
|
||||
if (changes.Count == 0) return null;
|
||||
|
||||
var menu = new ContextMenu();
|
||||
if (changes.Count == 1) {
|
||||
var change = changes[0];
|
||||
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
|
||||
|
||||
var explore = new MenuItem();
|
||||
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
|
||||
explore.Header = App.Text("RevealFile");
|
||||
explore.Icon = CreateMenuIcon("Icons.Folder.Open");
|
||||
explore.Click += (o, e) => {
|
||||
Native.OS.OpenInFileManager(path, true);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var openWith = new MenuItem();
|
||||
openWith.Header = App.Text("OpenWith");
|
||||
openWith.Icon = CreateMenuIcon("Icons.OpenWith");
|
||||
openWith.IsEnabled = File.Exists(path);
|
||||
openWith.Click += (_, e) => {
|
||||
Native.OS.OpenWithDefaultEditor(path);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var unstage = new MenuItem();
|
||||
unstage.Header = App.Text("FileCM.Unstage");
|
||||
unstage.Icon = CreateMenuIcon("Icons.File.Remove");
|
||||
unstage.Click += (o, e) => {
|
||||
UnstageChanges(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.Stash");
|
||||
stash.Icon = CreateMenuIcon("Icons.Stashes");
|
||||
stash.Click += (_, e) => {
|
||||
if (PopupHost.CanCreatePopup()) {
|
||||
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
|
||||
}
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var patch = new MenuItem();
|
||||
patch.Header = App.Text("FileCM.SaveAsPatch");
|
||||
patch.Icon = CreateMenuIcon("Icons.Diff");
|
||||
patch.Click += async (o, e) => {
|
||||
var topLevel = App.GetTopLevel();
|
||||
if (topLevel == null) return;
|
||||
|
||||
var options = new FilePickerSaveOptions();
|
||||
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) {
|
||||
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath));
|
||||
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var copyPath = new MenuItem();
|
||||
copyPath.Header = App.Text("CopyPath");
|
||||
copyPath.Icon = CreateMenuIcon("Icons.Copy");
|
||||
copyPath.Click += (o, e) => {
|
||||
App.CopyText(change.Path);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(explore);
|
||||
menu.Items.Add(openWith);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
menu.Items.Add(unstage);
|
||||
menu.Items.Add(stash);
|
||||
menu.Items.Add(patch);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
menu.Items.Add(copyPath);
|
||||
} else {
|
||||
var unstage = new MenuItem();
|
||||
unstage.Header = App.Text("FileCM.UnstageMulti", changes.Count);
|
||||
unstage.Icon = CreateMenuIcon("Icons.File.Remove");
|
||||
unstage.Click += (o, e) => {
|
||||
UnstageChanges(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
|
||||
stash.Icon = CreateMenuIcon("Icons.Stashes");
|
||||
stash.Click += (_, e) => {
|
||||
if (PopupHost.CanCreatePopup()) {
|
||||
PopupHost.ShowPopup(new StashChanges(_repo, changes, false));
|
||||
}
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var patch = new MenuItem();
|
||||
patch.Header = App.Text("FileCM.SaveAsPatch");
|
||||
patch.Icon = CreateMenuIcon("Icons.Diff");
|
||||
patch.Click += async (_, e) => {
|
||||
var topLevel = App.GetTopLevel();
|
||||
if (topLevel == null) return;
|
||||
|
||||
var options = new FilePickerSaveOptions();
|
||||
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) {
|
||||
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.Exec(_repo.FullPath, changes, false, storageFile.Path.LocalPath));
|
||||
if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(unstage);
|
||||
menu.Items.Add(stash);
|
||||
menu.Items.Add(patch);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private Avalonia.Controls.Shapes.Path CreateMenuIcon(string key) {
|
||||
var icon = new Avalonia.Controls.Shapes.Path();
|
||||
icon.Width = 12;
|
||||
icon.Height = 12;
|
||||
icon.Stretch = Stretch.Uniform;
|
||||
icon.Data = App.Current?.FindResource(key) as StreamGeometry;
|
||||
return icon;
|
||||
}
|
||||
|
||||
private void PushCommitMessage() {
|
||||
var existIdx = _repo.CommitMessages.IndexOf(CommitMessage);
|
||||
if (existIdx == 0) {
|
||||
return;
|
||||
} else if (existIdx > 0) {
|
||||
_repo.CommitMessages.Move(existIdx, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_repo.CommitMessages.Count > 9) {
|
||||
_repo.CommitMessages.RemoveRange(9, _repo.CommitMessages.Count - 9);
|
||||
}
|
||||
|
||||
_repo.CommitMessages.Insert(0, CommitMessage);
|
||||
}
|
||||
|
||||
private Repository _repo = null;
|
||||
private bool _isLoadingData = false;
|
||||
private bool _isStaging = false;
|
||||
private bool _isUnstaging = false;
|
||||
private bool _isCommitting = false;
|
||||
private bool _useAmend = false;
|
||||
private List<Models.Change> _unstaged = null;
|
||||
private List<Models.Change> _staged = null;
|
||||
private int _count = 0;
|
||||
private List<FileTreeNode> _unstagedTree = null;
|
||||
private List<FileTreeNode> _stagedTree = null;
|
||||
private ViewChangeDetailContext _lastViewChange = null;
|
||||
private object _detailContext = null;
|
||||
private string _commitMessage = string.Empty;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue