feature: add worktree support (#205)

This commit is contained in:
leo 2024-06-27 18:25:16 +08:00
parent 43af8c49a1
commit 8a8aabede3
No known key found for this signature in database
23 changed files with 959 additions and 21 deletions

View file

@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public partial class AddWorktree : Popup
{
[GeneratedRegex(@"^[\w\-/\.]+$")]
private static partial Regex REG_NAME();
[Required(ErrorMessage = "Worktree path is required!")]
[CustomValidation(typeof(AddWorktree), nameof(ValidateWorktreePath))]
public string FullPath
{
get => _fullPath;
set => SetProperty(ref _fullPath, value, true);
}
[CustomValidation(typeof(AddWorktree), nameof(ValidateBranchName))]
public string CustomName
{
get => _customName;
set => SetProperty(ref _customName, value, true);
}
public bool SetTrackingBranch
{
get => _setTrackingBranch;
set => SetProperty(ref _setTrackingBranch, value);
}
public List<string> TrackingBranches
{
get;
private set;
}
public string SelectedTrackingBranch
{
get;
set;
}
public AddWorktree(Repository repo)
{
_repo = repo;
TrackingBranches = new List<string>();
foreach (var branch in repo.Branches)
{
if (!branch.IsLocal)
TrackingBranches.Add($"{branch.Remote}/{branch.Name}");
}
if (TrackingBranches.Count > 0)
SelectedTrackingBranch = TrackingBranches[0];
else
SelectedTrackingBranch = string.Empty;
View = new Views.AddWorktree() { DataContext = this };
}
public static ValidationResult ValidateWorktreePath(string folder, ValidationContext _)
{
var info = new DirectoryInfo(folder);
if (info.Exists)
{
var files = info.GetFiles();
if (files.Length > 0)
return new ValidationResult("Given path is not empty!!!");
var folders = info.GetDirectories();
if (folders.Length > 0)
return new ValidationResult("Given path is not empty!!!");
}
return ValidationResult.Success;
}
public static ValidationResult ValidateBranchName(string name, ValidationContext ctx)
{
if (string.IsNullOrEmpty(name))
return ValidationResult.Success;
var creator = ctx.ObjectInstance as AddWorktree;
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);
ProgressDescription = "Adding worktree ...";
var tracking = _setTrackingBranch ? SelectedTrackingBranch : string.Empty;
return Task.Run(() =>
{
var succ = new Commands.Worktree(_repo.FullPath).Add(_fullPath, _customName, tracking, SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
private string _fullPath = string.Empty;
private string _customName = string.Empty;
private bool _setTrackingBranch = false;
}
}

View file

@ -0,0 +1,44 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public class LockWorktree : Popup
{
public Models.Worktree Target
{
get;
private set;
} = null;
public string Reason
{
get;
set;
} = string.Empty;
public LockWorktree(Repository repo, Models.Worktree target)
{
_repo = repo;
Target = target;
View = new Views.LockWorktree() { DataContext = this };
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Locking worktrees ...";
return Task.Run(() =>
{
var succ = new Commands.Worktree(_repo.FullPath).Lock(Target.FullPath, Reason);
if (succ)
Target.IsLocked = true;
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private Repository _repo = null;
}
}

View file

@ -0,0 +1,28 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public class PruneWorktrees : Popup
{
public PruneWorktrees(Repository repo)
{
_repo = repo;
View = new Views.PruneWorktrees() { DataContext = this };
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Prune worktrees ...";
return Task.Run(() =>
{
new Commands.Worktree(_repo.FullPath).Prune(SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
});
}
private Repository _repo = null;
}
}

View file

@ -0,0 +1,41 @@
using System.Threading.Tasks;
namespace SourceGit.ViewModels
{
public class RemoveWorktree : Popup
{
public Models.Worktree Target
{
get;
private set;
} = null;
public bool Force
{
get;
set;
} = false;
public RemoveWorktree(Repository repo, Models.Worktree target)
{
_repo = repo;
Target = target;
View = new Views.RemoveWorktree() { DataContext = this };
}
public override Task<bool> Sure()
{
_repo.SetWatcherEnabled(false);
ProgressDescription = "Remove worktrees ...";
return Task.Run(() =>
{
var succ = new Commands.Worktree(_repo.FullPath).Remove(Target.FullPath, Force, SetProgressDescription);
CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ;
});
}
private Repository _repo = null;
}
}

View file

@ -131,6 +131,13 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _remoteBranchTrees, value);
}
[JsonIgnore]
public List<Models.Worktree> Worktrees
{
get => _worktrees;
private set => SetProperty(ref _worktrees, value);
}
[JsonIgnore]
public List<Models.Tag> Tags
{
@ -219,6 +226,13 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _isSubmoduleGroupExpanded, value);
}
[JsonIgnore]
public bool IsWorktreeGroupExpanded
{
get => _isWorktreeGroupExpanded;
set => SetProperty(ref _isWorktreeGroupExpanded, value);
}
[JsonIgnore]
public InProgressContext InProgressContext
{
@ -295,6 +309,7 @@ namespace SourceGit.ViewModels
});
Task.Run(RefreshSubmodules);
Task.Run(RefreshWorktrees);
Task.Run(RefreshWorkingCopyChanges);
Task.Run(RefreshStashes);
}
@ -590,11 +605,31 @@ namespace SourceGit.ViewModels
});
}
public void RefreshWorktrees()
{
var worktrees = new Commands.Worktree(_fullpath).List();
var cleaned = new List<Models.Worktree>();
foreach (var worktree in worktrees)
{
if (worktree.IsBare || worktree.FullPath.Equals(_fullpath))
continue;
cleaned.Add(worktree);
}
Dispatcher.UIThread.Invoke(() =>
{
Worktrees = cleaned;
});
}
public void RefreshTags()
{
var tags = new Commands.QueryTags(FullPath).Result();
foreach (var tag in tags)
tag.IsFiltered = Filters.Contains(tag.Name);
Dispatcher.UIThread.Invoke(() =>
{
Tags = tags;
@ -656,10 +691,7 @@ namespace SourceGit.ViewModels
public void RefreshSubmodules()
{
var submodules = new Commands.QuerySubmodules(FullPath).Result();
Dispatcher.UIThread.Invoke(() =>
{
Submodules = submodules;
});
Dispatcher.UIThread.Invoke(() => Submodules = submodules);
}
public void RefreshWorkingCopyChanges()
@ -732,6 +764,16 @@ namespace SourceGit.ViewModels
public void CheckoutBranch(Models.Branch branch)
{
if (branch.IsLocal)
{
var worktree = _worktrees.Find(x => x.Branch == branch.FullName);
if (worktree != null)
{
OpenWorktree(worktree);
return;
}
}
if (!PopupHost.CanCreatePopup())
return;
@ -817,6 +859,36 @@ namespace SourceGit.ViewModels
}
}
public void AddWorktree()
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new AddWorktree(this));
}
public void PruneWorktrees()
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowAndStartPopup(new PruneWorktrees(this));
}
public void OpenWorktree(Models.Worktree worktree)
{
var gitDir = new Commands.QueryGitDir(worktree.FullPath).Result();
var repo = Preference.AddRepository(worktree.FullPath, gitDir);
var node = new RepositoryNode()
{
Id = repo.FullPath,
Name = Path.GetFileName(repo.FullPath),
Bookmark = 0,
IsRepository = true,
};
var launcher = App.GetTopLevel().DataContext as Launcher;
if (launcher != null)
launcher.OpenRepositoryInTab(node, null);
}
public ContextMenu CreateContextMenuForGitFlow()
{
var menu = new ContextMenu();
@ -1260,9 +1332,8 @@ namespace SourceGit.ViewModels
target.Click += (o, e) =>
{
if (Commands.Branch.SetUpstream(_fullpath, branch.Name, upstream))
{
Task.Run(RefreshBranches);
}
e.Handled = true;
};
@ -1274,9 +1345,8 @@ namespace SourceGit.ViewModels
unsetUpstream.Click += (_, e) =>
{
if (Commands.Branch.SetUpstream(_fullpath, branch.Name, string.Empty))
{
Task.Run(RefreshBranches);
}
e.Handled = true;
};
tracking.Items.Add(new MenuItem() { Header = "-" });
@ -1634,6 +1704,65 @@ namespace SourceGit.ViewModels
return menu;
}
public ContextMenu CreateContextMenuForWorktree(Models.Worktree worktree)
{
var menu = new ContextMenu();
if (worktree.IsLocked)
{
var unlock = new MenuItem();
unlock.Header = App.Text("Worktree.Unlock");
unlock.Icon = App.CreateMenuIcon("Icons.Unlock");
unlock.Click += (o, ev) =>
{
SetWatcherEnabled(false);
var succ = new Commands.Worktree(_fullpath).Unlock(worktree.FullPath);
if (succ)
worktree.IsLocked = false;
SetWatcherEnabled(true);
ev.Handled = true;
};
menu.Items.Add(unlock);
}
else
{
var loc = new MenuItem();
loc.Header = App.Text("Worktree.Lock");
loc.Icon = App.CreateMenuIcon("Icons.Lock");
loc.Click += (o, ev) =>
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new LockWorktree(this, worktree));
ev.Handled = true;
};
menu.Items.Add(loc);
}
var remove = new MenuItem();
remove.Header = App.Text("Worktree.Remove");
remove.Icon = App.CreateMenuIcon("Icons.Clear");
remove.Click += (o, ev) =>
{
if (PopupHost.CanCreatePopup())
PopupHost.ShowPopup(new RemoveWorktree(this, worktree));
ev.Handled = true;
};
menu.Items.Add(remove);
var copy = new MenuItem();
copy.Header = App.Text("Worktree.CopyPath");
copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (o, e) =>
{
App.CopyText(worktree.FullPath);
e.Handled = true;
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(copy);
return menu;
}
private MenuItem CreateMenuItemToCompareBranches(Models.Branch branch)
{
if (Branches.Count == 1)
@ -1712,6 +1841,7 @@ namespace SourceGit.ViewModels
private bool _isTagGroupExpanded = false;
private bool _isSubmoduleGroupExpanded = false;
private bool _isWorktreeGroupExpanded = false;
private string _searchBranchFilter = string.Empty;
@ -1719,6 +1849,7 @@ namespace SourceGit.ViewModels
private List<Models.Branch> _branches = new List<Models.Branch>();
private List<BranchTreeNode> _localBranchTrees = new List<BranchTreeNode>();
private List<BranchTreeNode> _remoteBranchTrees = new List<BranchTreeNode>();
private List<Models.Worktree> _worktrees = new List<Models.Worktree>();
private List<Models.Tag> _tags = new List<Models.Tag>();
private List<string> _submodules = new List<string>();
private bool _includeUntracked = true;