mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-23 21:24:59 +00:00
feature: add worktree support (#205)
This commit is contained in:
parent
43af8c49a1
commit
8a8aabede3
23 changed files with 959 additions and 21 deletions
122
src/ViewModels/AddWorktree.cs
Normal file
122
src/ViewModels/AddWorktree.cs
Normal 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;
|
||||
}
|
||||
}
|
44
src/ViewModels/LockWorktree.cs
Normal file
44
src/ViewModels/LockWorktree.cs
Normal 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;
|
||||
}
|
||||
}
|
28
src/ViewModels/PruneWorktrees.cs
Normal file
28
src/ViewModels/PruneWorktrees.cs
Normal 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;
|
||||
}
|
||||
}
|
41
src/ViewModels/RemoveWorktree.cs
Normal file
41
src/ViewModels/RemoveWorktree.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue