mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-23 05:05:00 +00:00
project: reorganize the structure of the project.
* remove dotnet-tool.json because the project does not rely on any dotnet tools. * remove Directory.Build.props because the solution has only one project. * move src/SourceGit to src. It's not needed to put all sources into a subfolder of src since there's only one project.
This commit is contained in:
parent
96e60da7ad
commit
96d4150d26
319 changed files with 37 additions and 53 deletions
107
src/ViewModels/AddRemote.cs
Normal file
107
src/ViewModels/AddRemote.cs
Normal file
|
@ -0,0 +1,107 @@
|
|||
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);
|
||||
ProgressDescription = "Adding remote ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
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.MarkBranchesDirtyManually();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
});
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private string _name = string.Empty;
|
||||
private string _url = string.Empty;
|
||||
private bool _useSSH = false;
|
||||
}
|
||||
}
|
76
src/ViewModels/AddSubmodule.cs
Normal file
76
src/ViewModels/AddSubmodule.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
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);
|
||||
ProgressDescription = "Adding submodule...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Submodule(_repo.FullPath).Add(_url, _relativePath, Recursive, SetProgressDescription);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private string _url = string.Empty;
|
||||
private string _relativePath = string.Empty;
|
||||
}
|
||||
}
|
78
src/ViewModels/Apply.cs
Normal file
78
src/ViewModels/Apply.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
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);
|
||||
ProgressDescription = "Apply patch...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, null).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private string _patchFile = string.Empty;
|
||||
private bool _ignoreWhiteSpace = true;
|
||||
}
|
||||
}
|
73
src/ViewModels/Archive.cs
Normal file
73
src/ViewModels/Archive.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
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);
|
||||
ProgressDescription = "Archiving ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Archive(_repo.FullPath, _revision, _saveFile, SetProgressDescription).Exec();
|
||||
CallUIThread(() =>
|
||||
{
|
||||
_repo.SetWatcherEnabled(true);
|
||||
if (succ)
|
||||
App.SendNotification(_repo.FullPath, $"Save archive to : {_saveFile}");
|
||||
});
|
||||
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private string _saveFile = string.Empty;
|
||||
private readonly string _revision = string.Empty;
|
||||
}
|
||||
}
|
38
src/ViewModels/AssumeUnchangedManager.cs
Normal file
38
src/ViewModels/AssumeUnchangedManager.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Threading;
|
||||
|
||||
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 readonly string _repo;
|
||||
}
|
||||
}
|
61
src/ViewModels/Blame.cs
Normal file
61
src/ViewModels/Blame.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
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 readonly string _repo = string.Empty;
|
||||
private string _selectedSHA = string.Empty;
|
||||
private Models.BlameData _data = null;
|
||||
}
|
||||
}
|
35
src/ViewModels/Checkout.cs
Normal file
35
src/ViewModels/Checkout.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
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);
|
||||
ProgressDescription = $"Checkout '{Branch}' ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Checkout(_repo.FullPath).Branch(Branch, SetProgressDescription);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo;
|
||||
}
|
||||
}
|
42
src/ViewModels/CherryPick.cs
Normal file
42
src/ViewModels/CherryPick.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
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);
|
||||
ProgressDescription = $"Cherry-Pick commit '{Target.SHA}' ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.CherryPick(_repo.FullPath, Target.SHA, !AutoCommit).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
36
src/ViewModels/Cleanup.cs
Normal file
36
src/ViewModels/Cleanup.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
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(() =>
|
||||
{
|
||||
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 readonly Repository _repo = null;
|
||||
}
|
||||
}
|
28
src/ViewModels/ClearStashes.cs
Normal file
28
src/ViewModels/ClearStashes.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
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);
|
||||
ProgressDescription = "Clear all stashes...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
new Commands.Stash(_repo.FullPath).Clear();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
142
src/ViewModels/Clone.cs
Normal file
142
src/ViewModels/Clone.cs
Normal file
|
@ -0,0 +1,142 @@
|
|||
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(Launcher launcher, LauncherPage page)
|
||||
{
|
||||
_launcher = launcher;
|
||||
_page = page;
|
||||
|
||||
View = new Views.Clone() { DataContext = this };
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
ProgressDescription = "Clone ...";
|
||||
|
||||
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 = repo.FullPath,
|
||||
Name = Path.GetFileName(repo.FullPath),
|
||||
Bookmark = 0,
|
||||
IsRepository = true,
|
||||
};
|
||||
Preference.AddNode(node);
|
||||
|
||||
_launcher.OpenRepositoryInTab(node, _page);
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Launcher _launcher = null;
|
||||
private readonly 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;
|
||||
}
|
||||
}
|
537
src/ViewModels/CommitDetail.cs
Normal file
537
src/ViewModels/CommitDetail.cs
Normal file
|
@ -0,0 +1,537 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class CommitDetail : ObservableObject
|
||||
{
|
||||
public DiffContext DiffContext
|
||||
{
|
||||
get => _diffContext;
|
||||
private set => SetProperty(ref _diffContext, value);
|
||||
}
|
||||
|
||||
public int ActivePageIndex
|
||||
{
|
||||
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), _diffContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 Cleanup()
|
||||
{
|
||||
_repo = null;
|
||||
_commit = null;
|
||||
if (_changes != null)
|
||||
_changes.Clear();
|
||||
if (_visibleChanges != null)
|
||||
_visibleChanges.Clear();
|
||||
if (_changeTree != null)
|
||||
_changeTree.Clear();
|
||||
_selectedChange = null;
|
||||
_selectedChangeNode = null;
|
||||
_searchChangeFilter = null;
|
||||
_diffContext = null;
|
||||
if (_revisionFiles != null)
|
||||
_revisionFiles.Clear();
|
||||
if (_revisionFilesTree != null)
|
||||
_revisionFilesTree.Clear();
|
||||
_selectedRevisionFileNode = null;
|
||||
_searchFileFilter = null;
|
||||
_viewRevisionFileContent = null;
|
||||
_cancelToken = null;
|
||||
}
|
||||
|
||||
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 = App.CreateMenuIcon("Icons.Histories");
|
||||
history.Click += (_, ev) =>
|
||||
{
|
||||
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
|
||||
window.Show();
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var blame = new MenuItem();
|
||||
blame.Header = App.Text("Blame");
|
||||
blame.Icon = App.CreateMenuIcon("Icons.Blame");
|
||||
blame.Click += (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 = App.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 = App.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 = App.CreateMenuIcon("Icons.Histories");
|
||||
history.Click += (_, ev) =>
|
||||
{
|
||||
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, file.Path) };
|
||||
window.Show();
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var blame = new MenuItem();
|
||||
blame.Header = App.Text("Blame");
|
||||
blame.Icon = App.CreateMenuIcon("Icons.Blame");
|
||||
blame.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 = App.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 = App.CreateMenuIcon("Icons.Save");
|
||||
saveAs.IsEnabled = file.Type == Models.ObjectType.Blob;
|
||||
saveAs.Click += async (_, ev) =>
|
||||
{
|
||||
var topLevel = App.GetTopLevel();
|
||||
if (topLevel == null)
|
||||
return;
|
||||
|
||||
var options = new FolderPickerOpenOptions() { AllowMultiple = false };
|
||||
var selected = await topLevel.StorageProvider.OpenFolderPickerAsync(options);
|
||||
if (selected.Count == 1)
|
||||
{
|
||||
var saveTo = Path.Combine(selected[0].Path.LocalPath, Path.GetFileName(file.Path));
|
||||
Commands.SaveRevisionFile.Run(_repo, _commit.SHA, file.Path, saveTo);
|
||||
}
|
||||
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var copyPath = new MenuItem();
|
||||
copyPath.Header = App.Text("CopyPath");
|
||||
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
|
||||
copyPath.Click += (_, ev) =>
|
||||
{
|
||||
App.CopyText(file.Path);
|
||||
ev.Handled = true;
|
||||
};
|
||||
|
||||
var 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)
|
||||
{
|
||||
var ext = Path.GetExtension(file.Path);
|
||||
if (IMG_EXTS.Contains(ext))
|
||||
{
|
||||
var bitmap = Commands.GetImageFileAsBitmap.Run(_repo, _commit.SHA, file.Path);
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
ViewRevisionFileContent = new Models.RevisionImageFile() { Image = bitmap };
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var size = new Commands.QueryFileSize(_repo, file.Path, _commit.SHA).Result();
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
ViewRevisionFileContent = new Models.RevisionBinaryFile() { Size = size };
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var content = new Commands.QueryFileContent(_repo, _commit.SHA, file.Path).Result();
|
||||
if (content.StartsWith("version https://git-lfs.github.com/spec/", StringComparison.Ordinal))
|
||||
{
|
||||
var obj = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
|
||||
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lines.Length == 3)
|
||||
{
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.StartsWith("oid sha256:", StringComparison.Ordinal))
|
||||
{
|
||||
obj.Object.Oid = line.Substring(11);
|
||||
}
|
||||
else if (line.StartsWith("size ", StringComparison.Ordinal))
|
||||
{
|
||||
obj.Object.Size = long.Parse(line.Substring(5));
|
||||
}
|
||||
}
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
ViewRevisionFileContent = obj;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
ViewRevisionFileContent = new Models.RevisionTextFile()
|
||||
{
|
||||
FileName = file.Path,
|
||||
Content = content
|
||||
};
|
||||
});
|
||||
});
|
||||
break;
|
||||
case Models.ObjectType.Commit:
|
||||
ViewRevisionFileContent = new Models.RevisionSubmodule() { SHA = file.SHA };
|
||||
break;
|
||||
default:
|
||||
ViewRevisionFileContent = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> IMG_EXTS = new HashSet<string>()
|
||||
{
|
||||
".ico", ".bmp", ".jpg", ".png", ".jpeg"
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
141
src/ViewModels/CreateBranch.cs
Normal file
141
src/ViewModels/CreateBranch.cs
Normal file
|
@ -0,0 +1,141 @@
|
|||
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.FullName;
|
||||
|
||||
if (!branch.IsLocal && repo.Branches.Find(x => x.IsLocal && x.Name == branch.Name) == null)
|
||||
{
|
||||
Name = branch.Name;
|
||||
}
|
||||
|
||||
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 readonly Repository _repo = null;
|
||||
private string _name = null;
|
||||
private readonly string _baseOnRevision = null;
|
||||
}
|
||||
}
|
38
src/ViewModels/CreateGroup.cs
Normal file
38
src/ViewModels/CreateGroup.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
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 readonly RepositoryNode _parent = null;
|
||||
private string _name = string.Empty;
|
||||
}
|
||||
}
|
78
src/ViewModels/CreateTag.cs
Normal file
78
src/ViewModels/CreateTag.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
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);
|
||||
ProgressDescription = "Create tag...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
Commands.Tag.Add(_repo.FullPath, TagName, _basedOn, Message);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private string _tagName = string.Empty;
|
||||
private readonly string _basedOn = string.Empty;
|
||||
}
|
||||
}
|
43
src/ViewModels/DeleteBranch.cs
Normal file
43
src/ViewModels/DeleteBranch.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
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);
|
||||
ProgressDescription = "Deleting branch...";
|
||||
|
||||
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 readonly Repository _repo = null;
|
||||
}
|
||||
}
|
39
src/ViewModels/DeleteRemote.cs
Normal file
39
src/ViewModels/DeleteRemote.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
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);
|
||||
ProgressDescription = "Deleting remote ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Remote(_repo.FullPath).Delete(Remote.Name);
|
||||
CallUIThread(() =>
|
||||
{
|
||||
_repo.MarkBranchesDirtyManually();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
});
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
27
src/ViewModels/DeleteRepositoryNode.cs
Normal file
27
src/ViewModels/DeleteRepositoryNode.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
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;
|
||||
}
|
||||
}
|
36
src/ViewModels/DeleteSubmodule.cs
Normal file
36
src/ViewModels/DeleteSubmodule.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
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);
|
||||
ProgressDescription = "Deleting submodule ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Submodule(_repo.FullPath).Delete(Submodule);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
43
src/ViewModels/DeleteTag.cs
Normal file
43
src/ViewModels/DeleteTag.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
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);
|
||||
ProgressDescription = $"Deleting tag '{Target.Name}' ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var remotes = ShouldPushToRemote ? _repo.Remotes : null;
|
||||
var succ = Commands.Tag.Delete(_repo.FullPath, Target.Name, remotes);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
178
src/ViewModels/DiffContext.cs
Normal file
178
src/ViewModels/DiffContext.cs
Normal file
|
@ -0,0 +1,178 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class DiffContext : ObservableObject
|
||||
{
|
||||
public string RepositoryPath
|
||||
{
|
||||
get => _repo;
|
||||
}
|
||||
|
||||
public Models.Change WorkingCopyChange
|
||||
{
|
||||
get => _option.WorkingCopyChange;
|
||||
}
|
||||
|
||||
public bool IsUnstaged
|
||||
{
|
||||
get => _option.IsUnstaged;
|
||||
}
|
||||
|
||||
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 IsTextDiff
|
||||
{
|
||||
get => _isTextDiff;
|
||||
private set => SetProperty(ref _isTextDiff, value);
|
||||
}
|
||||
|
||||
public object Content
|
||||
{
|
||||
get => _content;
|
||||
private set => SetProperty(ref _content, value);
|
||||
}
|
||||
|
||||
public Vector SyncScrollOffset
|
||||
{
|
||||
get => _syncScrollOffset;
|
||||
set => SetProperty(ref _syncScrollOffset, value);
|
||||
}
|
||||
|
||||
public DiffContext(string repo, Models.DiffOption option, DiffContext previous = null)
|
||||
{
|
||||
_repo = repo;
|
||||
_option = option;
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
_isTextDiff = previous._isTextDiff;
|
||||
_content = previous._content;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(FilePath));
|
||||
OnPropertyChanged(nameof(IsOrgFilePathVisible));
|
||||
OnPropertyChanged(nameof(OrgFilePath));
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
var latest = new Commands.Diff(repo, option).Result();
|
||||
var rs = null as object;
|
||||
|
||||
if (latest.TextDiff != null)
|
||||
{
|
||||
latest.TextDiff.File = _option.Path;
|
||||
rs = latest.TextDiff;
|
||||
}
|
||||
else if (latest.IsBinary)
|
||||
{
|
||||
var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath;
|
||||
var ext = Path.GetExtension(oldPath);
|
||||
|
||||
if (IMG_EXTS.Contains(ext))
|
||||
{
|
||||
var imgDiff = new Models.ImageDiff();
|
||||
if (option.Revisions.Count == 2)
|
||||
{
|
||||
imgDiff.Old = Commands.GetImageFileAsBitmap.Run(repo, option.Revisions[0], oldPath);
|
||||
imgDiff.New = Commands.GetImageFileAsBitmap.Run(repo, option.Revisions[1], oldPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fullPath = Path.Combine(repo, _option.Path);
|
||||
imgDiff.Old = Commands.GetImageFileAsBitmap.Run(repo, "HEAD", oldPath);
|
||||
imgDiff.New = File.Exists(fullPath) ? new Bitmap(fullPath) : null;
|
||||
}
|
||||
rs = imgDiff;
|
||||
}
|
||||
else
|
||||
{
|
||||
var binaryDiff = new Models.BinaryDiff();
|
||||
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
|
||||
{
|
||||
var fullPath = Path.Combine(repo, _option.Path);
|
||||
binaryDiff.OldSize = new Commands.QueryFileSize(repo, oldPath, "HEAD").Result();
|
||||
binaryDiff.NewSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0;
|
||||
}
|
||||
rs = binaryDiff;
|
||||
}
|
||||
}
|
||||
else if (latest.IsLFS)
|
||||
{
|
||||
rs = latest.LFSDiff;
|
||||
}
|
||||
else
|
||||
{
|
||||
rs = new Models.NoOrEOLChange();
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Content = rs;
|
||||
IsTextDiff = latest.TextDiff != null;
|
||||
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 static readonly HashSet<string> IMG_EXTS = new HashSet<string>()
|
||||
{
|
||||
".ico", ".bmp", ".jpg", ".png", ".jpeg"
|
||||
};
|
||||
|
||||
private readonly string _repo = string.Empty;
|
||||
private readonly Models.DiffOption _option = null;
|
||||
private bool _isLoading = true;
|
||||
private bool _isTextDiff = false;
|
||||
private object _content = null;
|
||||
private Vector _syncScrollOffset = Vector.Zero;
|
||||
}
|
||||
}
|
78
src/ViewModels/Discard.cs
Normal file
78
src/ViewModels/Discard.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
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)
|
||||
{
|
||||
_repo = repo;
|
||||
|
||||
Mode = new DiscardModeAll();
|
||||
View = new Views.Discard { DataContext = this };
|
||||
}
|
||||
|
||||
public Discard(Repository repo, List<Models.Change> changes, bool isUnstaged)
|
||||
{
|
||||
_repo = repo;
|
||||
_changes = changes;
|
||||
_isUnstaged = isUnstaged;
|
||||
|
||||
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);
|
||||
ProgressDescription = _changes == null ? "Discard all local changes ..." : $"Discard total {_changes.Count} changes ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (_changes == null)
|
||||
{
|
||||
Commands.Discard.All(_repo.FullPath);
|
||||
}
|
||||
else if (_isUnstaged)
|
||||
{
|
||||
Commands.Discard.ChangesInWorkTree(_repo.FullPath, _changes);
|
||||
}
|
||||
else
|
||||
{
|
||||
Commands.Discard.ChangesInStaged(_repo.FullPath, _changes);
|
||||
}
|
||||
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private readonly List<Models.Change> _changes = null;
|
||||
private readonly bool _isUnstaged = true;
|
||||
}
|
||||
}
|
29
src/ViewModels/DropStash.cs
Normal file
29
src/ViewModels/DropStash.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
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()
|
||||
{
|
||||
ProgressDescription = $"Dropping stash: {Stash.Name}";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
new Commands.Stash(_repo).Drop(Stash.Name);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly string _repo;
|
||||
}
|
||||
}
|
126
src/ViewModels/EditRemote.cs
Normal file
126
src/ViewModels/EditRemote.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
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);
|
||||
ProgressDescription = $"Editing remote '{_remote.Name}' ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (_remote.Name != _name)
|
||||
{
|
||||
var succ = new Commands.Remote(_repo.FullPath).Rename(_remote.Name, _name);
|
||||
if (succ)
|
||||
_remote.Name = _name;
|
||||
}
|
||||
|
||||
if (_remote.URL != _url)
|
||||
{
|
||||
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 readonly Repository _repo = null;
|
||||
private readonly Models.Remote _remote = null;
|
||||
private string _name = string.Empty;
|
||||
private string _url = string.Empty;
|
||||
private bool _useSSH = false;
|
||||
}
|
||||
}
|
64
src/ViewModels/EditRepositoryNode.cs
Normal file
64
src/ViewModels/EditRepositoryNode.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
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;
|
||||
}
|
||||
}
|
42
src/ViewModels/FastForwardWithoutCheckout.cs
Normal file
42
src/ViewModels/FastForwardWithoutCheckout.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
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);
|
||||
ProgressDescription = "Fast-Forward ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
new Commands.Fetch(_repo.FullPath, To.Remote, Local.Name, To.Name, SetProgressDescription).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
67
src/ViewModels/Fetch.cs
Normal file
67
src/ViewModels/Fetch.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
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 = preferedRemote == null;
|
||||
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 readonly Repository _repo = null;
|
||||
private bool _fetchAllRemotes = true;
|
||||
}
|
||||
}
|
84
src/ViewModels/FileHistories.cs
Normal file
84
src/ViewModels/FileHistories.cs
Normal file
|
@ -0,0 +1,84 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class FileHistories : ObservableObject
|
||||
{
|
||||
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;
|
||||
DetailContext.Commit = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
DiffContext = new DiffContext(_repo, new Models.DiffOption(value, _file), _diffContext);
|
||||
DetailContext.Commit = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DiffContext DiffContext
|
||||
{
|
||||
get => _diffContext;
|
||||
set => SetProperty(ref _diffContext, value);
|
||||
}
|
||||
|
||||
public CommitDetail DetailContext
|
||||
{
|
||||
get => _detailContext;
|
||||
set => SetProperty(ref _detailContext, value);
|
||||
}
|
||||
|
||||
public FileHistories(string repo, string file)
|
||||
{
|
||||
_repo = repo;
|
||||
_file = file;
|
||||
_detailContext = new CommitDetail(repo);
|
||||
|
||||
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];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private readonly string _repo = string.Empty;
|
||||
private readonly string _file = string.Empty;
|
||||
private bool _isLoading = true;
|
||||
private List<Models.Commit> _commits = null;
|
||||
private Models.Commit _selectedCommit = null;
|
||||
private DiffContext _diffContext = null;
|
||||
private CommitDetail _detailContext = null;
|
||||
}
|
||||
}
|
217
src/ViewModels/FileTreeNode.cs
Normal file
217
src/ViewModels/FileTreeNode.cs
Normal file
|
@ -0,0 +1,217 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class FileTreeNode : ObservableObject
|
||||
{
|
||||
public string FullPath { get; set; } = string.Empty;
|
||||
public bool IsFolder { get; set; } = false;
|
||||
public object Backend { get; set; } = null;
|
||||
public List<FileTreeNode> Children { get; set; } = new List<FileTreeNode>();
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
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('/', StringComparison.Ordinal);
|
||||
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.TryGetValue(folder, out var value))
|
||||
{
|
||||
lastFolder = value;
|
||||
}
|
||||
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('/', StringComparison.Ordinal);
|
||||
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.TryGetValue(folder, out var value))
|
||||
{
|
||||
lastFolder = value;
|
||||
}
|
||||
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 + "/", StringComparison.Ordinal))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
55
src/ViewModels/GitFlowFinish.cs
Normal file
55
src/ViewModels/GitFlowFinish.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
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 readonly Repository _repo = null;
|
||||
private readonly Models.Branch _branch = null;
|
||||
private readonly Models.GitFlowBranchType _type = Models.GitFlowBranchType.None;
|
||||
}
|
||||
}
|
79
src/ViewModels/GitFlowStart.cs
Normal file
79
src/ViewModels/GitFlowStart.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
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 readonly Repository _repo = null;
|
||||
private readonly Models.GitFlowBranchType _type = Models.GitFlowBranchType.Feature;
|
||||
private readonly string _prefix = string.Empty;
|
||||
private string _name = null;
|
||||
}
|
||||
}
|
602
src/ViewModels/Histories.cs
Normal file
602
src/ViewModels/Histories.cs
Normal file
|
@ -0,0 +1,602 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class CountSelectedCommits
|
||||
{
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class Histories : ObservableObject
|
||||
{
|
||||
public bool IsLoading
|
||||
{
|
||||
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 Cleanup()
|
||||
{
|
||||
Commits = new List<Models.Commit>();
|
||||
|
||||
_repo = null;
|
||||
_graph = null;
|
||||
_autoSelectedCommit = null;
|
||||
|
||||
if (_detailContext is CommitDetail cd)
|
||||
{
|
||||
cd.Cleanup();
|
||||
}
|
||||
else if (_detailContext is RevisionCompare rc)
|
||||
{
|
||||
rc.Cleanup();
|
||||
}
|
||||
|
||||
_detailContext = null;
|
||||
}
|
||||
|
||||
public void NavigateTo(string commitSHA)
|
||||
{
|
||||
var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal));
|
||||
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 = new Views.NameHighlightedTextBlock("CommitCM.Reset", current.Name);
|
||||
reset.Icon = App.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 = App.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 = App.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 = new Views.NameHighlightedTextBlock("CommitCM.Rebase", current.Name);
|
||||
rebase.Icon = App.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 = App.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 = App.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 = App.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 = App.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 = App.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 = App.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 = App.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 = App.CreateMenuIcon("Icons.Branch");
|
||||
submenu.Header = current.Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(current.Upstream))
|
||||
{
|
||||
var upstream = current.Upstream.Substring(13);
|
||||
|
||||
var fastForward = new MenuItem();
|
||||
fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", upstream);
|
||||
fastForward.Icon = App.CreateMenuIcon("Icons.FastForward");
|
||||
fastForward.IsEnabled = !string.IsNullOrEmpty(current.UpstreamTrackStatus) && current.UpstreamTrackStatus.IndexOf('↑') < 0;
|
||||
;
|
||||
fastForward.Click += (o, e) =>
|
||||
{
|
||||
if (PopupHost.CanCreatePopup())
|
||||
PopupHost.ShowAndStartPopup(new Merge(_repo, upstream, current.Name));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(fastForward);
|
||||
|
||||
var pull = new MenuItem();
|
||||
pull.Header = new Views.NameHighlightedTextBlock("BranchCM.Pull", upstream);
|
||||
pull.Icon = App.CreateMenuIcon("Icons.Pull");
|
||||
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 = new Views.NameHighlightedTextBlock("BranchCM.Push", current.Name);
|
||||
push.Icon = App.CreateMenuIcon("Icons.Push");
|
||||
push.IsEnabled = _repo.Remotes.Count > 0;
|
||||
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 = new Views.NameHighlightedTextBlock("BranchCM.Finish", current.Name);
|
||||
finish.Icon = App.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 = new Views.NameHighlightedTextBlock("BranchCM.Rename", current.Name);
|
||||
rename.Icon = App.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 = App.CreateMenuIcon("Icons.Branch");
|
||||
submenu.Header = branch.Name;
|
||||
|
||||
var checkout = new MenuItem();
|
||||
checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", branch.Name);
|
||||
checkout.Icon = App.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 = new Views.NameHighlightedTextBlock("BranchCM.Merge", branch.Name, current.Name);
|
||||
merge.Icon = App.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 = new Views.NameHighlightedTextBlock("BranchCM.Finish", branch.Name);
|
||||
finish.Icon = App.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 = new Views.NameHighlightedTextBlock("BranchCM.Rename", branch.Name);
|
||||
rename.Icon = App.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 = new Views.NameHighlightedTextBlock("BranchCM.Delete", branch.Name);
|
||||
delete.Icon = App.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 = App.CreateMenuIcon("Icons.Branch");
|
||||
submenu.Header = name;
|
||||
|
||||
var checkout = new MenuItem();
|
||||
checkout.Header = new Views.NameHighlightedTextBlock("BranchCM.Checkout", name);
|
||||
checkout.Icon = App.CreateMenuIcon("Icons.Check");
|
||||
checkout.Click += (o, e) =>
|
||||
{
|
||||
foreach (var b in _repo.Branches)
|
||||
{
|
||||
if (b.IsLocal && b.Upstream == branch.FullName)
|
||||
{
|
||||
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 = new Views.NameHighlightedTextBlock("BranchCM.Merge", name, current.Name);
|
||||
merge.Icon = App.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 = new Views.NameHighlightedTextBlock("BranchCM.Delete", name);
|
||||
delete.Icon = App.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 = App.CreateMenuIcon("Icons.Tag");
|
||||
submenu.MinWidth = 200;
|
||||
|
||||
var push = new MenuItem();
|
||||
push.Header = new Views.NameHighlightedTextBlock("TagCM.Push", tag.Name);
|
||||
push.Icon = App.CreateMenuIcon("Icons.Push");
|
||||
push.IsEnabled = _repo.Remotes.Count > 0;
|
||||
push.Click += (o, e) =>
|
||||
{
|
||||
if (PopupHost.CanCreatePopup())
|
||||
PopupHost.ShowPopup(new PushTag(_repo, tag));
|
||||
e.Handled = true;
|
||||
};
|
||||
submenu.Items.Add(push);
|
||||
|
||||
var delete = new MenuItem();
|
||||
delete.Header = new Views.NameHighlightedTextBlock("TagCM.Delete", tag.Name);
|
||||
delete.Icon = App.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 Repository _repo = null;
|
||||
private readonly double _dataGridRowHeight = 28;
|
||||
private bool _isLoading = true;
|
||||
private List<Models.Commit> _commits = new List<Models.Commit>();
|
||||
private Models.CommitGraph _graph = null;
|
||||
private Models.Commit _autoSelectedCommit = null;
|
||||
private object _detailContext = null;
|
||||
}
|
||||
}
|
89
src/ViewModels/InProgressContexts.cs
Normal file
89
src/ViewModels/InProgressContexts.cs
Normal file
|
@ -0,0 +1,89 @@
|
|||
using System.IO;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public abstract class InProgressContext
|
||||
{
|
||||
public string Repository
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string Cmd
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public InProgressContext(string repo, string cmd)
|
||||
{
|
||||
Repository = repo;
|
||||
Cmd = cmd;
|
||||
}
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
new Commands.Command()
|
||||
{
|
||||
WorkingDirectory = Repository,
|
||||
Context = Repository,
|
||||
Args = $"{Cmd} --abort",
|
||||
}.Exec();
|
||||
}
|
||||
|
||||
public virtual bool Continue()
|
||||
{
|
||||
return new Commands.Command()
|
||||
{
|
||||
WorkingDirectory = Repository,
|
||||
Context = Repository,
|
||||
Args = $"-c core.editor=true {Cmd} --continue",
|
||||
}.Exec();
|
||||
}
|
||||
}
|
||||
|
||||
public class CherryPickInProgress : InProgressContext
|
||||
{
|
||||
public CherryPickInProgress(string repo) : base(repo, "cherry-pick") { }
|
||||
}
|
||||
|
||||
public class RebaseInProgress : InProgressContext
|
||||
{
|
||||
public RebaseInProgress(Repository repo) : base(repo.FullPath, "rebase")
|
||||
{
|
||||
_gitDir = repo.GitDir;
|
||||
}
|
||||
|
||||
public override bool Continue()
|
||||
{
|
||||
var succ = base.Continue();
|
||||
if (succ)
|
||||
{
|
||||
var rebaseMergeHead = Path.Combine(_gitDir, "REBASE_HEAD");
|
||||
var rebaseMergeFolder = Path.Combine(_gitDir, "rebase-merge");
|
||||
var rebaseApplyFolder = Path.Combine(_gitDir, "rebase-apply");
|
||||
if (File.Exists(rebaseMergeHead))
|
||||
File.Delete(rebaseMergeHead);
|
||||
if (Directory.Exists(rebaseMergeFolder))
|
||||
Directory.Delete(rebaseMergeFolder);
|
||||
if (Directory.Exists(rebaseApplyFolder))
|
||||
Directory.Delete(rebaseApplyFolder);
|
||||
}
|
||||
|
||||
return succ;
|
||||
}
|
||||
|
||||
private string _gitDir;
|
||||
}
|
||||
|
||||
public class RevertInProgress : InProgressContext
|
||||
{
|
||||
public RevertInProgress(string repo) : base(repo, "revert") { }
|
||||
}
|
||||
|
||||
public class MergeInProgress : InProgressContext
|
||||
{
|
||||
public MergeInProgress(string repo) : base(repo, "merge") { }
|
||||
}
|
||||
}
|
51
src/ViewModels/Init.cs
Normal file
51
src/ViewModels/Init.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
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()
|
||||
{
|
||||
ProgressDescription = $"Initialize git repository at: '{_targetPath}'";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
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 = repo.FullPath,
|
||||
Name = Path.GetFileName(repo.FullPath),
|
||||
Bookmark = 0,
|
||||
IsRepository = true,
|
||||
};
|
||||
Preference.AddNode(node);
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private string _targetPath;
|
||||
}
|
||||
}
|
117
src/ViewModels/InitGitFlow.cs
Normal file
117
src/ViewModels/InitGitFlow.cs
Normal file
|
@ -0,0 +1,117 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public partial class InitGitFlow : Popup
|
||||
{
|
||||
|
||||
[GeneratedRegex(@"^[\w\-/\.]+$")]
|
||||
private static partial Regex TAG_PREFIX();
|
||||
|
||||
[Required(ErrorMessage = "Master branch name is required!!!")]
|
||||
[RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")]
|
||||
[CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))]
|
||||
public string Master
|
||||
{
|
||||
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);
|
||||
ProgressDescription = "Init git-flow ...";
|
||||
|
||||
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 readonly 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;
|
||||
}
|
||||
}
|
243
src/ViewModels/Launcher.cs
Normal file
243
src/ViewModels/Launcher.cs
Normal file
|
@ -0,0 +1,243 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
using Avalonia.Collections;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
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();
|
||||
|
||||
if (Preference.Instance.RestoreTabs)
|
||||
{
|
||||
foreach (var id in Preference.Instance.OpenedTabs)
|
||||
{
|
||||
var node = Preference.FindNode(id);
|
||||
if (node == null)
|
||||
continue;
|
||||
|
||||
OpenRepositoryInTab(node, null);
|
||||
}
|
||||
|
||||
var lastActiveIdx = Preference.Instance.LastActiveTabIdx;
|
||||
if (lastActiveIdx >= 0 && lastActiveIdx < Pages.Count)
|
||||
{
|
||||
ActivePage = Pages[lastActiveIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Quit()
|
||||
{
|
||||
Preference.Instance.OpenedTabs.Clear();
|
||||
|
||||
if (Preference.Instance.RestoreTabs)
|
||||
{
|
||||
foreach (var page in Pages)
|
||||
{
|
||||
if (page.Node.IsRepository)
|
||||
Preference.Instance.OpenedTabs.Add(page.Node.Id);
|
||||
}
|
||||
}
|
||||
|
||||
Preference.Instance.LastActiveTabIdx = Pages.IndexOf(ActivePage);
|
||||
Preference.Save();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
CloseRepositoryInTab(page);
|
||||
Pages.RemoveAt(removeIdx);
|
||||
OnPropertyChanged(nameof(Pages));
|
||||
}
|
||||
else if (removeIdx + 1 == activeIdx)
|
||||
{
|
||||
CloseRepositoryInTab(page);
|
||||
Pages.RemoveAt(removeIdx);
|
||||
OnPropertyChanged(nameof(Pages));
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseRepositoryInTab(page);
|
||||
Pages.RemoveAt(removeIdx);
|
||||
}
|
||||
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
public void CloseOtherTabs(object param)
|
||||
{
|
||||
if (Pages.Count == 1)
|
||||
return;
|
||||
|
||||
var page = param as LauncherPage;
|
||||
if (page == null)
|
||||
page = _activePage;
|
||||
|
||||
ActivePage = page;
|
||||
|
||||
foreach (var one in Pages)
|
||||
{
|
||||
if (one.Node.Id != page.Node.Id)
|
||||
CloseRepositoryInTab(one);
|
||||
}
|
||||
|
||||
Pages = new AvaloniaList<LauncherPage> { page };
|
||||
OnPropertyChanged(nameof(Pages));
|
||||
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
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();
|
||||
Commands.AutoFetch.AddRepository(repo.FullPath);
|
||||
|
||||
if (page == null)
|
||||
{
|
||||
if (ActivePage == null || ActivePage.Node.IsRepository)
|
||||
{
|
||||
page = new LauncherPage(node, repo);
|
||||
Pages.Add(page);
|
||||
}
|
||||
else
|
||||
{
|
||||
page = ActivePage;
|
||||
page.Node = node;
|
||||
page.Data = repo;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
page.Node = node;
|
||||
page.Data = repo;
|
||||
}
|
||||
|
||||
ActivePage = page;
|
||||
}
|
||||
|
||||
private void CloseRepositoryInTab(LauncherPage page)
|
||||
{
|
||||
if (page.Data is Repository repo)
|
||||
{
|
||||
Commands.AutoFetch.RemoveRepository(repo.FullPath);
|
||||
repo.Close();
|
||||
}
|
||||
|
||||
page.Data = null;
|
||||
}
|
||||
|
||||
private LauncherPage _activePage = null;
|
||||
}
|
||||
}
|
67
src/ViewModels/LauncherPage.cs
Normal file
67
src/ViewModels/LauncherPage.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
|
||||
using Avalonia.Collections;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class LauncherPage : PopupHost
|
||||
{
|
||||
public RepositoryNode Node
|
||||
{
|
||||
get => _node;
|
||||
set => SetProperty(ref _node, value);
|
||||
}
|
||||
|
||||
public object Data
|
||||
{
|
||||
get => _data;
|
||||
set => SetProperty(ref _data, 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,
|
||||
};
|
||||
_data = new Welcome();
|
||||
}
|
||||
|
||||
public LauncherPage(RepositoryNode node, Repository repo)
|
||||
{
|
||||
_node = node;
|
||||
_data = 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 _data = null;
|
||||
}
|
||||
}
|
76
src/ViewModels/Merge.cs
Normal file
76
src/ViewModels/Merge.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
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);
|
||||
ProgressDescription = $"Merging '{Source}' into '{Into}' ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Merge(_repo.FullPath, Source, SelectedMode.Arg, SetProgressDescription).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
64
src/ViewModels/Popup.cs
Normal file
64
src/ViewModels/Popup.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
82
src/ViewModels/PopupHost.cs
Normal file
82
src/ViewModels/PopupHost.cs
Normal file
|
@ -0,0 +1,82 @@
|
|||
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 void ShowAndStartPopup(Popup popup)
|
||||
{
|
||||
var dumpPage = Active;
|
||||
popup.HostPageId = dumpPage.GetId();
|
||||
dumpPage.Popup = popup;
|
||||
dumpPage.ProcessPopup();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
446
src/ViewModels/Preference.cs
Normal file
446
src/ViewModels/Preference.cs
Normal file
|
@ -0,0 +1,446 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Media;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
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), JsonCodeGen.Default.Preference);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_instance = new Preference();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_instance.Repositories.RemoveAll(x => !Directory.Exists(x.FullPath));
|
||||
|
||||
if (_instance.DefaultFont == null)
|
||||
{
|
||||
_instance.DefaultFont = FontManager.Current.DefaultFontFamily;
|
||||
}
|
||||
|
||||
if (_instance.MonospaceFont == null)
|
||||
{
|
||||
_instance.MonospaceFont = new FontFamily("fonts:SourceGit#JetBrains Mono");
|
||||
}
|
||||
|
||||
if (!_instance.IsGitConfigured)
|
||||
{
|
||||
_instance.GitInstallPath = Native.OS.FindGitExecutable();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(FontFamilyConverter))]
|
||||
public FontFamily DefaultFont
|
||||
{
|
||||
get => _defaultFont;
|
||||
set => SetProperty(ref _defaultFont, value);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(FontFamilyConverter))]
|
||||
public FontFamily MonospaceFont
|
||||
{
|
||||
get => _monospaceFont;
|
||||
set => SetProperty(ref _monospaceFont, value);
|
||||
}
|
||||
|
||||
public double DefaultFontSize
|
||||
{
|
||||
get => _defaultFontSize;
|
||||
set => SetProperty(ref _defaultFontSize, value);
|
||||
}
|
||||
|
||||
public string AvatarServer
|
||||
{
|
||||
get => Models.AvatarManager.SelectedServer;
|
||||
set
|
||||
{
|
||||
if (Models.AvatarManager.SelectedServer != value)
|
||||
{
|
||||
Models.AvatarManager.SelectedServer = value;
|
||||
OnPropertyChanged(nameof(AvatarServer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MaxHistoryCommits
|
||||
{
|
||||
get => _maxHistoryCommits;
|
||||
set => SetProperty(ref _maxHistoryCommits, value);
|
||||
}
|
||||
|
||||
public bool RestoreTabs
|
||||
{
|
||||
get => _restoreTabs;
|
||||
set => SetProperty(ref _restoreTabs, value);
|
||||
}
|
||||
|
||||
public bool UseFixedTabWidth
|
||||
{
|
||||
get => _useFixedTabWidth;
|
||||
set => SetProperty(ref _useFixedTabWidth, value);
|
||||
}
|
||||
|
||||
public bool Check4UpdatesOnStartup
|
||||
{
|
||||
get => _check4UpdatesOnStartup;
|
||||
set => SetProperty(ref _check4UpdatesOnStartup, value);
|
||||
}
|
||||
|
||||
public string IgnoreUpdateTag
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = string.Empty;
|
||||
|
||||
public bool UseTwoColumnsLayoutInHistories
|
||||
{
|
||||
get => _useTwoColumnsLayoutInHistories;
|
||||
set => SetProperty(ref _useTwoColumnsLayoutInHistories, value);
|
||||
}
|
||||
|
||||
public bool UseSideBySideDiff
|
||||
{
|
||||
get => _useSideBySideDiff;
|
||||
set => SetProperty(ref _useSideBySideDiff, value);
|
||||
}
|
||||
|
||||
public bool UseSyntaxHighlighting
|
||||
{
|
||||
get => _useSyntaxHighlighting;
|
||||
set => SetProperty(ref _useSyntaxHighlighting, 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(GitInstallPath) && File.Exists(GitInstallPath);
|
||||
}
|
||||
|
||||
public string GitInstallPath
|
||||
{
|
||||
get => Native.OS.GitInstallPath;
|
||||
set
|
||||
{
|
||||
if (Native.OS.GitInstallPath != value)
|
||||
{
|
||||
Native.OS.GitInstallPath = value;
|
||||
OnPropertyChanged(nameof(GitInstallPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GitDefaultCloneDir
|
||||
{
|
||||
get => _gitDefaultCloneDir;
|
||||
set => SetProperty(ref _gitDefaultCloneDir, value);
|
||||
}
|
||||
|
||||
public bool GitAutoFetch
|
||||
{
|
||||
get => Commands.AutoFetch.IsEnabled;
|
||||
set
|
||||
{
|
||||
if (Commands.AutoFetch.IsEnabled != value)
|
||||
{
|
||||
Commands.AutoFetch.IsEnabled = value;
|
||||
OnPropertyChanged(nameof(GitAutoFetch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ExternalMergeToolType
|
||||
{
|
||||
get => _externalMergeToolType;
|
||||
set
|
||||
{
|
||||
var changed = SetProperty(ref _externalMergeToolType, value);
|
||||
if (changed && !OperatingSystem.IsWindows() && value > 0 && value < Models.ExternalMergeTools.Supported.Count)
|
||||
{
|
||||
var tool = Models.ExternalMergeTools.Supported[value];
|
||||
if (File.Exists(tool.Exec))
|
||||
ExternalMergeToolPath = tool.Exec;
|
||||
else
|
||||
ExternalMergeToolPath = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 List<string> OpenedTabs
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new List<string>();
|
||||
|
||||
public int LastActiveTabIdx
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = 0;
|
||||
|
||||
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 RepositoryNode FindNode(string id)
|
||||
{
|
||||
return FindNodeRecursive(id, _instance.RepositoryNodes);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
foreach (var repo in _instance.Repositories)
|
||||
{
|
||||
if (repo.FullPath == path)
|
||||
return repo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Repository AddRepository(string rootDir, string gitDir)
|
||||
{
|
||||
var normalized = rootDir.Replace('\\', '/');
|
||||
var repo = FindRepository(normalized);
|
||||
if (repo != null)
|
||||
{
|
||||
repo.GitDir = gitDir;
|
||||
return repo;
|
||||
}
|
||||
|
||||
repo = new Repository()
|
||||
{
|
||||
FullPath = normalized,
|
||||
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, JsonCodeGen.Default.Preference);
|
||||
File.WriteAllText(_savePath, data);
|
||||
}
|
||||
|
||||
private static RepositoryNode FindNodeRecursive(string id, AvaloniaList<RepositoryNode> collection)
|
||||
{
|
||||
foreach (var node in collection)
|
||||
{
|
||||
if (node.Id == id)
|
||||
return node;
|
||||
|
||||
var sub = FindNodeRecursive(id, node.SubNodes);
|
||||
if (sub != null)
|
||||
return sub;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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 FontFamily _defaultFont = null;
|
||||
private FontFamily _monospaceFont = null;
|
||||
private double _defaultFontSize = 13;
|
||||
|
||||
private int _maxHistoryCommits = 20000;
|
||||
private bool _restoreTabs = false;
|
||||
private bool _useFixedTabWidth = true;
|
||||
private bool _check4UpdatesOnStartup = true;
|
||||
private bool _useTwoColumnsLayoutInHistories = false;
|
||||
private bool _useSideBySideDiff = false;
|
||||
private bool _useSyntaxHighlighting = false;
|
||||
|
||||
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 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>();
|
||||
}
|
||||
|
||||
public class FontFamilyConverter : JsonConverter<FontFamily>
|
||||
{
|
||||
public override FontFamily Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var name = reader.GetString();
|
||||
return new FontFamily(name);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, FontFamily value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
35
src/ViewModels/PruneRemote.cs
Normal file
35
src/ViewModels/PruneRemote.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
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);
|
||||
ProgressDescription = "Run `prune` on remote ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Remote(_repo.FullPath).Prune(Remote.Name);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
158
src/ViewModels/Pull.cs
Normal file
158
src/ViewModels/Pull.cs
Normal file
|
@ -0,0 +1,158 @@
|
|||
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;
|
||||
} = true;
|
||||
|
||||
public bool AutoStash
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
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 readonly Repository _repo = null;
|
||||
private readonly Models.Branch _current = null;
|
||||
private Models.Remote _selectedRemote = null;
|
||||
private List<Models.Branch> _remoteBranches = null;
|
||||
private Models.Branch _selectedBranch = null;
|
||||
}
|
||||
}
|
216
src/ViewModels/Push.cs
Normal file
216
src/ViewModels/Push.cs
Normal file
|
@ -0,0 +1,216 @@
|
|||
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);
|
||||
|
||||
var remoteBranchName = _selectedRemoteBranch.Name.Replace(" (new)", "");
|
||||
ProgressDescription = $"Push {_selectedLocalBranch.Name} -> {_selectedRemote.Name}/{remoteBranchName} ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
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 readonly Repository _repo = null;
|
||||
private Models.Branch _selectedLocalBranch = null;
|
||||
private Models.Remote _selectedRemote = null;
|
||||
private List<Models.Branch> _remoteBranches = new List<Models.Branch>();
|
||||
private Models.Branch _selectedRemoteBranch = null;
|
||||
}
|
||||
}
|
48
src/ViewModels/PushTag.cs
Normal file
48
src/ViewModels/PushTag.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
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);
|
||||
ProgressDescription = $"Pushing tag '{Target.Name}' to remote '{SelectedRemote.Name}' ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Push(_repo.FullPath, SelectedRemote.Name, Target.Name, false).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
61
src/ViewModels/Rebase.cs
Normal file
61
src/ViewModels/Rebase.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
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);
|
||||
ProgressDescription = "Rebasing ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Rebase(_repo.FullPath, _revision, AutoStash).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private readonly string _revision = string.Empty;
|
||||
}
|
||||
}
|
66
src/ViewModels/RenameBranch.cs
Normal file
66
src/ViewModels/RenameBranch.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
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);
|
||||
ProgressDescription = $"Rename '{Target.Name}'";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = Commands.Branch.Rename(_repo.FullPath, Target.Name, _name);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private string _name = string.Empty;
|
||||
}
|
||||
}
|
1327
src/ViewModels/Repository.cs
Normal file
1327
src/ViewModels/Repository.cs
Normal file
File diff suppressed because it is too large
Load diff
88
src/ViewModels/RepositoryConfigure.cs
Normal file
88
src/ViewModels/RepositoryConfigure.cs
Normal file
|
@ -0,0 +1,88 @@
|
|||
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.TryGetValue("user.name", out var name))
|
||||
UserName = name;
|
||||
if (_cached.TryGetValue("user.email", out var email))
|
||||
UserEmail = email;
|
||||
if (_cached.TryGetValue("commit.gpgsign", out var gpgsign))
|
||||
GPGSigningEnabled = gpgsign == "true";
|
||||
if (_cached.TryGetValue("user.signingkey", out var signingKey))
|
||||
GPGUserSigningKey = signingKey;
|
||||
if (_cached.TryGetValue("http.proxy", out var proxy))
|
||||
HttpProxy = proxy;
|
||||
|
||||
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.TryGetValue(key, out var old))
|
||||
{
|
||||
changed = old != value;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
new Commands.Config(_repo.FullPath).Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private readonly Dictionary<string, string> _cached = null;
|
||||
}
|
||||
}
|
98
src/ViewModels/RepositoryNode.cs
Normal file
98
src/ViewModels/RepositoryNode.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
using Avalonia.Collections;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class RepositoryNode : ObservableObject
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
{
|
||||
var normalized = value.Replace('\\', '/');
|
||||
SetProperty(ref _id, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
}
|
79
src/ViewModels/Reset.cs
Normal file
79
src/ViewModels/Reset.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class ResetMode
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string Arg { get; set; }
|
||||
public IBrush Color { get; set; }
|
||||
|
||||
public ResetMode(string n, string d, string a, IBrush b)
|
||||
{
|
||||
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);
|
||||
ProgressDescription = $"Reset current branch to {To.SHA} ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Reset(_repo.FullPath, To.SHA, SelectedMode.Arg).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
42
src/ViewModels/Revert.cs
Normal file
42
src/ViewModels/Revert.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
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);
|
||||
ProgressDescription = $"Revert commit '{Target.SHA}' ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Revert(_repo.FullPath, Target.SHA, AutoCommit).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
}
|
||||
}
|
232
src/ViewModels/RevisionCompare.cs
Normal file
232
src/ViewModels/RevisionCompare.cs
Normal file
|
@ -0,0 +1,232 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class RevisionCompare : ObservableObject
|
||||
{
|
||||
public Models.Commit StartPoint
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Models.Commit EndPoint
|
||||
{
|
||||
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), _diffContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 Cleanup()
|
||||
{
|
||||
_repo = null;
|
||||
if (_changes != null)
|
||||
_changes.Clear();
|
||||
if (_visibleChanges != null)
|
||||
_visibleChanges.Clear();
|
||||
if (_changeTree != null)
|
||||
_changeTree.Clear();
|
||||
_selectedChange = null;
|
||||
_selectedNode = null;
|
||||
_searchFilter = null;
|
||||
_diffContext = null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
48
src/ViewModels/Reword.cs
Normal file
48
src/ViewModels/Reword.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
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);
|
||||
ProgressDescription = $"Editing head commit message ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var succ = new Commands.Commit(_repo.FullPath, _message, true, true).Exec();
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return succ;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private string _message = string.Empty;
|
||||
}
|
||||
}
|
15
src/ViewModels/SelfUpdate.cs
Normal file
15
src/ViewModels/SelfUpdate.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class SelfUpdate : ObservableObject
|
||||
{
|
||||
public object Data
|
||||
{
|
||||
get => _data;
|
||||
set => SetProperty(ref _data, value);
|
||||
}
|
||||
|
||||
private object _data = null;
|
||||
}
|
||||
}
|
54
src/ViewModels/Squash.cs
Normal file
54
src/ViewModels/Squash.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
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);
|
||||
ProgressDescription = "Squashing ...";
|
||||
|
||||
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 readonly Repository _repo = null;
|
||||
private string _message = string.Empty;
|
||||
}
|
||||
}
|
69
src/ViewModels/StashChanges.cs
Normal file
69
src/ViewModels/StashChanges.cs
Normal file
|
@ -0,0 +1,69 @@
|
|||
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);
|
||||
ProgressDescription = $"Stash changes ...";
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
new Commands.Stash(_repo.FullPath).Push(jobs, Message);
|
||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Repository _repo = null;
|
||||
private readonly List<Models.Change> _changes = null;
|
||||
}
|
||||
}
|
154
src/ViewModels/StashesPage.cs
Normal file
154
src/ViewModels/StashesPage.cs
Normal file
|
@ -0,0 +1,154 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class StashesPage : ObservableObject
|
||||
{
|
||||
public int Count
|
||||
{
|
||||
get => _stashes == null ? 0 : _stashes.Count;
|
||||
}
|
||||
|
||||
public List<Models.Stash> Stashes
|
||||
{
|
||||
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), _diffContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DiffContext DiffContext
|
||||
{
|
||||
get => _diffContext;
|
||||
private set => SetProperty(ref _diffContext, value);
|
||||
}
|
||||
|
||||
public StashesPage(Repository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
_repo = null;
|
||||
if (_stashes != null)
|
||||
_stashes.Clear();
|
||||
_selectedStash = null;
|
||||
if (_changes != null)
|
||||
_changes.Clear();
|
||||
_selectedChange = null;
|
||||
_diffContext = null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
74
src/ViewModels/Statistics.cs
Normal file
74
src/ViewModels/Statistics.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class Statistics : ObservableObject
|
||||
{
|
||||
public bool IsLoading
|
||||
{
|
||||
get => _isLoading;
|
||||
private set => SetProperty(ref _isLoading, value);
|
||||
}
|
||||
|
||||
public int SelectedIndex
|
||||
{
|
||||
get => _selectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedIndex, value))
|
||||
RefreshReport();
|
||||
}
|
||||
}
|
||||
|
||||
public Models.StatisticsReport SelectedReport
|
||||
{
|
||||
get => _selectedReport;
|
||||
private set => SetProperty(ref _selectedReport, value);
|
||||
}
|
||||
|
||||
public Statistics(string repo)
|
||||
{
|
||||
_repo = repo;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
var result = new Commands.Statistics(_repo).Result();
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
_data = result;
|
||||
RefreshReport();
|
||||
IsLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void RefreshReport()
|
||||
{
|
||||
if (_data == null)
|
||||
return;
|
||||
|
||||
switch (_selectedIndex)
|
||||
{
|
||||
case 0:
|
||||
SelectedReport = _data.Year;
|
||||
break;
|
||||
case 1:
|
||||
SelectedReport = _data.Month;
|
||||
break;
|
||||
default:
|
||||
SelectedReport = _data.Week;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly string _repo = string.Empty;
|
||||
private bool _isLoading = true;
|
||||
private Models.Statistics _data = null;
|
||||
private Models.StatisticsReport _selectedReport = null;
|
||||
private int _selectedIndex = 0;
|
||||
}
|
||||
}
|
56
src/ViewModels/TwoSideTextDiff.cs
Normal file
56
src/ViewModels/TwoSideTextDiff.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class TwoSideTextDiff : ObservableObject
|
||||
{
|
||||
public string File { get; set; } = string.Empty;
|
||||
public List<Models.TextDiffLine> Old { get; set; } = new List<Models.TextDiffLine>();
|
||||
public List<Models.TextDiffLine> New { get; set; } = new List<Models.TextDiffLine>();
|
||||
public int MaxLineNumber = 0;
|
||||
|
||||
public TwoSideTextDiff(Models.TextDiff diff)
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
143
src/ViewModels/Welcome.cs
Normal file
143
src/ViewModels/Welcome.cs
Normal file
|
@ -0,0 +1,143 @@
|
|||
using System;
|
||||
|
||||
using Avalonia.Collections;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class Welcome : ObservableObject
|
||||
{
|
||||
public bool IsClearSearchVisible
|
||||
{
|
||||
get => !string.IsNullOrEmpty(_searchFilter);
|
||||
}
|
||||
|
||||
public AvaloniaList<RepositoryNode> RepositoryNodes
|
||||
{
|
||||
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 launcher = param as Launcher;
|
||||
var page = launcher.ActivePage;
|
||||
|
||||
if (!Preference.Instance.IsGitConfigured)
|
||||
{
|
||||
App.RaiseException(page.GetId(), App.Text("NotConfigured"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (PopupHost.CanCreatePopup())
|
||||
{
|
||||
PopupHost.ShowPopup(new Clone(launcher, 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;
|
||||
}
|
||||
}
|
922
src/ViewModels/WorkingCopy.cs
Normal file
922
src/ViewModels/WorkingCopy.cs
Normal file
|
@ -0,0 +1,922 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.ViewModels
|
||||
{
|
||||
public class ConflictContext
|
||||
{
|
||||
public Models.Change Change { get; set; }
|
||||
}
|
||||
|
||||
public class WorkingCopy : ObservableObject
|
||||
{
|
||||
public bool IsStaging
|
||||
{
|
||||
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 Models.Change SelectedUnstagedChange
|
||||
{
|
||||
get => _selectedUnstagedChange;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedUnstagedChange, value) && value != null)
|
||||
{
|
||||
SelectedStagedChange = null;
|
||||
SelectedStagedTreeNode = null;
|
||||
SetDetail(value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Models.Change SelectedStagedChange
|
||||
{
|
||||
get => _selectedStagedChange;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedStagedChange, value) && value != null)
|
||||
{
|
||||
SelectedUnstagedChange = null;
|
||||
SelectedUnstagedTreeNode = null;
|
||||
SetDetail(value, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 FileTreeNode SelectedUnstagedTreeNode
|
||||
{
|
||||
get => _selectedUnstagedTreeNode;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedUnstagedTreeNode, value))
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
SelectedUnstagedChange = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedUnstagedChange = value.Backend as Models.Change;
|
||||
SelectedStagedTreeNode = null;
|
||||
SelectedStagedChange = null;
|
||||
|
||||
if (value.IsFolder)
|
||||
{
|
||||
SetDetail(null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FileTreeNode SelectedStagedTreeNode
|
||||
{
|
||||
get => _selectedStagedTreeNode;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedStagedTreeNode, value))
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
SelectedStagedChange = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedStagedChange = value.Backend as Models.Change;
|
||||
SelectedUnstagedTreeNode = null;
|
||||
SelectedUnstagedChange = null;
|
||||
|
||||
if (value.IsFolder)
|
||||
{
|
||||
SetDetail(null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 void Cleanup()
|
||||
{
|
||||
_repo = null;
|
||||
if (_unstaged != null)
|
||||
_unstaged.Clear();
|
||||
if (_staged != null)
|
||||
_staged.Clear();
|
||||
if (_unstagedTree != null)
|
||||
_unstagedTree.Clear();
|
||||
if (_stagedTree != null)
|
||||
_stagedTree.Clear();
|
||||
_selectedUnstagedChange = null;
|
||||
_selectedStagedChange = null;
|
||||
_selectedUnstagedTreeNode = null;
|
||||
_selectedStagedTreeNode = null;
|
||||
_detailContext = null;
|
||||
_commitMessage = string.Empty;
|
||||
}
|
||||
|
||||
public bool SetData(List<Models.Change> changes)
|
||||
{
|
||||
var unstaged = new List<Models.Change>();
|
||||
var staged = new List<Models.Change>();
|
||||
|
||||
var viewFile = string.Empty;
|
||||
var lastSelectedIsUnstaged = false;
|
||||
if (_selectedUnstagedChange != null)
|
||||
{
|
||||
viewFile = _selectedUnstagedChange.Path;
|
||||
lastSelectedIsUnstaged = true;
|
||||
}
|
||||
else if (_selectedStagedChange != null)
|
||||
{
|
||||
viewFile = _selectedStagedChange.Path;
|
||||
}
|
||||
|
||||
var viewChange = null as Models.Change;
|
||||
var hasConflict = false;
|
||||
foreach (var c in changes)
|
||||
{
|
||||
if (c.Index == Models.ChangeState.Modified
|
||||
|| c.Index == Models.ChangeState.Added
|
||||
|| c.Index == Models.ChangeState.Deleted
|
||||
|| c.Index == Models.ChangeState.Renamed)
|
||||
{
|
||||
staged.Add(c);
|
||||
if (!lastSelectedIsUnstaged && c.Path == viewFile)
|
||||
{
|
||||
viewChange = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (c.WorkTree != Models.ChangeState.None)
|
||||
{
|
||||
unstaged.Add(c);
|
||||
hasConflict |= c.IsConflit;
|
||||
if (lastSelectedIsUnstaged && c.Path == viewFile)
|
||||
{
|
||||
viewChange = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_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;
|
||||
|
||||
// Restore last selection states.
|
||||
if (viewChange != null)
|
||||
{
|
||||
var scrollOffset = Vector.Zero;
|
||||
if (_detailContext is DiffContext old)
|
||||
scrollOffset = old.SyncScrollOffset;
|
||||
|
||||
if (lastSelectedIsUnstaged)
|
||||
{
|
||||
SelectedUnstagedChange = viewChange;
|
||||
SelectedUnstagedTreeNode = FileTreeNode.SelectByPath(_unstagedTree, viewFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedStagedChange = viewChange;
|
||||
SelectedStagedTreeNode = FileTreeNode.SelectByPath(_stagedTree, viewFile);
|
||||
}
|
||||
|
||||
if (_detailContext is DiffContext cur)
|
||||
cur.SyncScrollOffset = scrollOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedUnstagedChange = null;
|
||||
SelectedUnstagedTreeNode = null;
|
||||
SelectedStagedChange = null;
|
||||
SelectedStagedTreeNode = null;
|
||||
SetDetail(null, false);
|
||||
}
|
||||
});
|
||||
|
||||
return hasConflict;
|
||||
}
|
||||
|
||||
public void SetDetail(Models.Change change, bool isUnstaged)
|
||||
{
|
||||
if (_isLoadingData)
|
||||
return;
|
||||
|
||||
if (change == null)
|
||||
{
|
||||
DetailContext = null;
|
||||
}
|
||||
else if (change.IsConflit)
|
||||
{
|
||||
DetailContext = new ConflictContext() { Change = change };
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_detailContext is DiffContext previous)
|
||||
{
|
||||
DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), previous);
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
|
||||
SetDetail(null, true);
|
||||
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.MarkWorkingCopyDirtyManually();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
IsStaging = false;
|
||||
}
|
||||
|
||||
public async void UnstageChanges(List<Models.Change> changes)
|
||||
{
|
||||
if (_staged.Count == 0 || changes.Count == 0)
|
||||
return;
|
||||
|
||||
SetDetail(null, false);
|
||||
IsUnstaging = true;
|
||||
_repo.SetWatcherEnabled(false);
|
||||
if (changes.Count == _staged.Count)
|
||||
{
|
||||
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.MarkWorkingCopyDirtyManually();
|
||||
_repo.SetWatcherEnabled(true);
|
||||
IsUnstaging = false;
|
||||
}
|
||||
|
||||
public void Discard(List<Models.Change> changes, bool isUnstaged)
|
||||
{
|
||||
if (PopupHost.CanCreatePopup())
|
||||
{
|
||||
if (isUnstaged)
|
||||
{
|
||||
if (changes.Count == _unstaged.Count && _staged.Count == 0)
|
||||
{
|
||||
PopupHost.ShowPopup(new Discard(_repo));
|
||||
}
|
||||
else
|
||||
{
|
||||
PopupHost.ShowPopup(new Discard(_repo, changes, true));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (changes.Count == _staged.Count && _unstaged.Count == 0)
|
||||
{
|
||||
PopupHost.ShowPopup(new Discard(_repo));
|
||||
}
|
||||
else
|
||||
{
|
||||
PopupHost.ShowPopup(new Discard(_repo, changes, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.MarkWorkingCopyDirtyManually();
|
||||
_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.MarkWorkingCopyDirtyManually();
|
||||
_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();
|
||||
|
||||
SetDetail(null, false);
|
||||
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.MarkWorkingCopyDirtyManually();
|
||||
_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 = App.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 = App.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 = App.CreateMenuIcon("Icons.File.Add");
|
||||
stage.Click += (_, e) =>
|
||||
{
|
||||
StageChanges(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var discard = new MenuItem();
|
||||
discard.Header = App.Text("FileCM.Discard");
|
||||
discard.Icon = App.CreateMenuIcon("Icons.Undo");
|
||||
discard.Click += (_, e) =>
|
||||
{
|
||||
Discard(changes, true);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.Stash");
|
||||
stash.Icon = App.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 = App.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 = App.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 = App.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 = App.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 = App.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 = App.CreateMenuIcon("Icons.Undo");
|
||||
discard.Click += (_, e) =>
|
||||
{
|
||||
Discard(changes, true);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
|
||||
stash.Icon = App.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 = App.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 = App.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 = App.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 = App.CreateMenuIcon("Icons.File.Remove");
|
||||
unstage.Click += (o, e) =>
|
||||
{
|
||||
UnstageChanges(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var discard = new MenuItem();
|
||||
discard.Header = App.Text("FileCM.Discard");
|
||||
discard.Icon = App.CreateMenuIcon("Icons.Undo");
|
||||
discard.Click += (_, e) =>
|
||||
{
|
||||
Discard(changes, false);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.Stash");
|
||||
stash.Icon = App.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 = App.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 = App.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(discard);
|
||||
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 = App.CreateMenuIcon("Icons.File.Remove");
|
||||
unstage.Click += (o, e) =>
|
||||
{
|
||||
UnstageChanges(changes);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var discard = new MenuItem();
|
||||
discard.Header = App.Text("FileCM.DiscardMulti", changes.Count);
|
||||
discard.Icon = App.CreateMenuIcon("Icons.Undo");
|
||||
discard.Click += (_, e) =>
|
||||
{
|
||||
Discard(changes, false);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var stash = new MenuItem();
|
||||
stash.Header = App.Text("FileCM.StashMulti", changes.Count);
|
||||
stash.Icon = App.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 = App.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(discard);
|
||||
menu.Items.Add(stash);
|
||||
menu.Items.Add(patch);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
public ContextMenu CreateContextMenuForCommitMessages()
|
||||
{
|
||||
var menu = new ContextMenu();
|
||||
if (_repo.CommitMessages.Count == 0)
|
||||
{
|
||||
var empty = new MenuItem();
|
||||
empty.Header = App.Text("WorkingCopy.NoCommitHistories");
|
||||
empty.IsEnabled = false;
|
||||
menu.Items.Add(empty);
|
||||
return menu;
|
||||
}
|
||||
|
||||
var tip = new MenuItem();
|
||||
tip.Header = App.Text("WorkingCopy.HasCommitHistories");
|
||||
tip.IsEnabled = false;
|
||||
menu.Items.Add(tip);
|
||||
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||
|
||||
foreach (var message in _repo.CommitMessages)
|
||||
{
|
||||
var dump = message;
|
||||
|
||||
var item = new MenuItem();
|
||||
item.Header = dump;
|
||||
item.Click += (o, e) =>
|
||||
{
|
||||
CommitMessage = dump;
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
menu.Items.Add(item);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
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 Models.Change _selectedUnstagedChange = null;
|
||||
private Models.Change _selectedStagedChange = null;
|
||||
private int _count = 0;
|
||||
private List<FileTreeNode> _unstagedTree = null;
|
||||
private List<FileTreeNode> _stagedTree = null;
|
||||
private FileTreeNode _selectedUnstagedTreeNode = null;
|
||||
private FileTreeNode _selectedStagedTreeNode = null;
|
||||
private object _detailContext = null;
|
||||
private string _commitMessage = string.Empty;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue